Add support for pointer scrolling to trigger floats & snaps (#76145)
This commit is contained in:
parent
3cbfe82d9d
commit
ff15d04f21
@ -1067,58 +1067,6 @@ class _AppBarState extends State<AppBar> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FloatingAppBar extends StatefulWidget {
|
|
||||||
const _FloatingAppBar({ Key? key, required this.child }) : super(key: key);
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_FloatingAppBarState createState() => _FloatingAppBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A wrapper for the widget created by _SliverAppBarDelegate that starts and
|
|
||||||
// stops the floating app bar's snap-into-view or snap-out-of-view animation.
|
|
||||||
class _FloatingAppBarState extends State<_FloatingAppBar> {
|
|
||||||
ScrollPosition? _position;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
if (_position != null)
|
|
||||||
_position!.isScrollingNotifier.removeListener(_isScrollingListener);
|
|
||||||
_position = Scrollable.of(context)?.position;
|
|
||||||
if (_position != null)
|
|
||||||
_position!.isScrollingNotifier.addListener(_isScrollingListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
if (_position != null)
|
|
||||||
_position!.isScrollingNotifier.removeListener(_isScrollingListener);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderSliverFloatingPersistentHeader? _headerRenderer() {
|
|
||||||
return context.findAncestorRenderObjectOfType<RenderSliverFloatingPersistentHeader>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _isScrollingListener() {
|
|
||||||
if (_position == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// When a scroll stops, then maybe snap the appbar into view.
|
|
||||||
// Similarly, when a scroll starts, then maybe stop the snap animation.
|
|
||||||
final RenderSliverFloatingPersistentHeader? header = _headerRenderer();
|
|
||||||
if (_position!.isScrollingNotifier.value)
|
|
||||||
header?.maybeStopSnapAnimation(_position!.userScrollDirection);
|
|
||||||
else
|
|
||||||
header?.maybeStartSnapAnimation(_position!.userScrollDirection);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => widget.child;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||||
_SliverAppBarDelegate({
|
_SliverAppBarDelegate({
|
||||||
required this.leading,
|
required this.leading,
|
||||||
@ -1264,7 +1212,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
systemOverlayStyle: systemOverlayStyle,
|
systemOverlayStyle: systemOverlayStyle,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return floating ? _FloatingAppBar(child: appBar) : appBar;
|
return appBar;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -550,6 +550,10 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
|
|||||||
late Animation<double> _animation;
|
late Animation<double> _animation;
|
||||||
double? _lastActualScrollOffset;
|
double? _lastActualScrollOffset;
|
||||||
double? _effectiveScrollOffset;
|
double? _effectiveScrollOffset;
|
||||||
|
// Important for pointer scrolling, which does not have the same concept of
|
||||||
|
// a hold and release scroll movement, like dragging.
|
||||||
|
// This keeps track of the last ScrollDirection when scrolling started.
|
||||||
|
ScrollDirection? _lastStartedScrollDirection;
|
||||||
|
|
||||||
// Distance from our leading edge to the child's leading edge, in the axis
|
// Distance from our leading edge to the child's leading edge, in the axis
|
||||||
// direction. Negative if we're scrolled off the top.
|
// direction. Negative if we're scrolled off the top.
|
||||||
@ -647,6 +651,11 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the last known ScrollDirection when scrolling began.
|
||||||
|
void updateScrollStartDirection(ScrollDirection direction) {
|
||||||
|
_lastStartedScrollDirection = direction;
|
||||||
|
}
|
||||||
|
|
||||||
/// If the header isn't already fully exposed, then scroll it into view.
|
/// If the header isn't already fully exposed, then scroll it into view.
|
||||||
void maybeStartSnapAnimation(ScrollDirection direction) {
|
void maybeStartSnapAnimation(ScrollDirection direction) {
|
||||||
final FloatingHeaderSnapConfiguration? snap = snapConfiguration;
|
final FloatingHeaderSnapConfiguration? snap = snapConfiguration;
|
||||||
@ -680,7 +689,8 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
|
|||||||
(_effectiveScrollOffset! < maxExtent))) { // some part of it is visible, so should shrink or reveal as appropriate.
|
(_effectiveScrollOffset! < maxExtent))) { // some part of it is visible, so should shrink or reveal as appropriate.
|
||||||
double delta = _lastActualScrollOffset! - constraints.scrollOffset;
|
double delta = _lastActualScrollOffset! - constraints.scrollOffset;
|
||||||
|
|
||||||
final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward;
|
final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward
|
||||||
|
|| (_lastStartedScrollDirection != null && _lastStartedScrollDirection == ScrollDirection.forward);
|
||||||
if (allowFloatingExpansion) {
|
if (allowFloatingExpansion) {
|
||||||
if (_effectiveScrollOffset! > maxExtent) // We're scrolled off-screen, but should reveal, so
|
if (_effectiveScrollOffset! > maxExtent) // We're scrolled off-screen, but should reveal, so
|
||||||
_effectiveScrollOffset = maxExtent; // pretend we're just at the limit.
|
_effectiveScrollOffset = maxExtent; // pretend we're just at the limit.
|
||||||
|
@ -1103,6 +1103,13 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
|
|||||||
delta < 0.0 ? ScrollDirection.forward : ScrollDirection.reverse,
|
delta < 0.0 ? ScrollDirection.forward : ScrollDirection.reverse,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Set the isScrollingNotifier. Even if only one position actually receives
|
||||||
|
// the delta, the NestedScrollView's intention is to treat multiple
|
||||||
|
// ScrollPositions as one.
|
||||||
|
_outerPosition!.isScrollingNotifier.value = true;
|
||||||
|
for (final _NestedScrollPosition position in _innerPositions)
|
||||||
|
position.isScrollingNotifier.value = true;
|
||||||
|
|
||||||
if (_innerPositions.isEmpty) {
|
if (_innerPositions.isEmpty) {
|
||||||
// Does not enter overscroll.
|
// Does not enter overscroll.
|
||||||
_outerPosition!.applyClampedPointerSignalUpdate(delta);
|
_outerPosition!.applyClampedPointerSignalUpdate(delta);
|
||||||
|
@ -216,6 +216,7 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc
|
|||||||
);
|
);
|
||||||
final double oldPixels = pixels;
|
final double oldPixels = pixels;
|
||||||
forcePixels(targetPixels);
|
forcePixels(targetPixels);
|
||||||
|
isScrollingNotifier.value = true;
|
||||||
didStartScroll();
|
didStartScroll();
|
||||||
didUpdateScrollPositionBy(pixels - oldPixels);
|
didUpdateScrollPositionBy(pixels - oldPixels);
|
||||||
didEndScroll();
|
didEndScroll();
|
||||||
|
@ -7,6 +7,8 @@ import 'package:flutter/rendering.dart';
|
|||||||
import 'package:flutter/scheduler.dart' show TickerProvider;
|
import 'package:flutter/scheduler.dart' show TickerProvider;
|
||||||
|
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
|
import 'scroll_position.dart';
|
||||||
|
import 'scrollable.dart';
|
||||||
|
|
||||||
/// Delegate for configuring a [SliverPersistentHeader].
|
/// Delegate for configuring a [SliverPersistentHeader].
|
||||||
abstract class SliverPersistentHeaderDelegate {
|
abstract class SliverPersistentHeaderDelegate {
|
||||||
@ -185,8 +187,72 @@ class SliverPersistentHeader extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FloatingHeader extends StatefulWidget {
|
||||||
|
const _FloatingHeader({ Key? key, required this.child }) : super(key: key);
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_FloatingHeaderState createState() => _FloatingHeaderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wrapper for the widget created by _SliverPersistentHeaderElement that
|
||||||
|
// starts and stops the floating app bar's snap-into-view or snap-out-of-view
|
||||||
|
// animation. It also informs the float when pointer scrolling by updating the
|
||||||
|
// last known ScrollDirection when scrolling began.
|
||||||
|
class _FloatingHeaderState extends State<_FloatingHeader> {
|
||||||
|
ScrollPosition? _position;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
if (_position != null)
|
||||||
|
_position!.isScrollingNotifier.removeListener(_isScrollingListener);
|
||||||
|
_position = Scrollable.of(context)?.position;
|
||||||
|
if (_position != null)
|
||||||
|
_position!.isScrollingNotifier.addListener(_isScrollingListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_position != null)
|
||||||
|
_position!.isScrollingNotifier.removeListener(_isScrollingListener);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderSliverFloatingPersistentHeader? _headerRenderer() {
|
||||||
|
return context.findAncestorRenderObjectOfType<RenderSliverFloatingPersistentHeader>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _isScrollingListener() {
|
||||||
|
assert(_position != null);
|
||||||
|
|
||||||
|
// When a scroll stops, then maybe snap the app bar into view.
|
||||||
|
// Similarly, when a scroll starts, then maybe stop the snap animation.
|
||||||
|
// Update the scrolling direction as well for pointer scrolling updates.
|
||||||
|
final RenderSliverFloatingPersistentHeader? header = _headerRenderer();
|
||||||
|
if (_position!.isScrollingNotifier.value) {
|
||||||
|
header?.updateScrollStartDirection(_position!.userScrollDirection);
|
||||||
|
// Only SliverAppBars support snapping, headers will not snap.
|
||||||
|
header?.maybeStopSnapAnimation(_position!.userScrollDirection);
|
||||||
|
} else {
|
||||||
|
// Only SliverAppBars support snapping, headers will not snap.
|
||||||
|
header?.maybeStartSnapAnimation(_position!.userScrollDirection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => widget.child;
|
||||||
|
}
|
||||||
|
|
||||||
class _SliverPersistentHeaderElement extends RenderObjectElement {
|
class _SliverPersistentHeaderElement extends RenderObjectElement {
|
||||||
_SliverPersistentHeaderElement(_SliverPersistentHeaderRenderObjectWidget widget) : super(widget);
|
_SliverPersistentHeaderElement(
|
||||||
|
_SliverPersistentHeaderRenderObjectWidget widget, {
|
||||||
|
this.floating = false,
|
||||||
|
}) : assert(floating != null),
|
||||||
|
super(widget);
|
||||||
|
|
||||||
|
final bool floating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_SliverPersistentHeaderRenderObjectWidget get widget => super.widget as _SliverPersistentHeaderRenderObjectWidget;
|
_SliverPersistentHeaderRenderObjectWidget get widget => super.widget as _SliverPersistentHeaderRenderObjectWidget;
|
||||||
@ -229,11 +295,13 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
|
|||||||
owner!.buildScope(this, () {
|
owner!.buildScope(this, () {
|
||||||
child = updateChild(
|
child = updateChild(
|
||||||
child,
|
child,
|
||||||
widget.delegate.build(
|
floating
|
||||||
this,
|
? _FloatingHeader(child: widget.delegate.build(
|
||||||
shrinkOffset,
|
this,
|
||||||
overlapsContent,
|
shrinkOffset,
|
||||||
),
|
overlapsContent
|
||||||
|
))
|
||||||
|
: widget.delegate.build(this, shrinkOffset, overlapsContent),
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -273,13 +341,16 @@ abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWid
|
|||||||
const _SliverPersistentHeaderRenderObjectWidget({
|
const _SliverPersistentHeaderRenderObjectWidget({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.delegate,
|
required this.delegate,
|
||||||
|
this.floating = false,
|
||||||
}) : assert(delegate != null),
|
}) : assert(delegate != null),
|
||||||
|
assert(floating != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
final SliverPersistentHeaderDelegate delegate;
|
final SliverPersistentHeaderDelegate delegate;
|
||||||
|
final bool floating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_SliverPersistentHeaderElement createElement() => _SliverPersistentHeaderElement(this);
|
_SliverPersistentHeaderElement createElement() => _SliverPersistentHeaderElement(this, floating: floating);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context);
|
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context);
|
||||||
@ -383,6 +454,7 @@ class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjec
|
|||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
delegate: delegate,
|
delegate: delegate,
|
||||||
|
floating: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -428,6 +500,7 @@ class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRende
|
|||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
delegate: delegate,
|
delegate: delegate,
|
||||||
|
floating: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1490,6 +1490,60 @@ void main() {
|
|||||||
verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true);
|
verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('snap with pointer signal', (WidgetTester tester) async {
|
||||||
|
final GlobalKey appBarKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildFloatTest(
|
||||||
|
floating: true,
|
||||||
|
snap: true,
|
||||||
|
appBarKey: appBarKey,
|
||||||
|
));
|
||||||
|
|
||||||
|
final Offset scrollEventLocation = tester.getCenter(find.byType(NestedScrollView));
|
||||||
|
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
||||||
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
||||||
|
testPointer.hover(scrollEventLocation);
|
||||||
|
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsOneWidget);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
56.0,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true);
|
||||||
|
|
||||||
|
// Scroll away the outer scroll view and some of the inner scroll view.
|
||||||
|
// We will not scroll back the same amount to indicate that we are
|
||||||
|
// snapping in before reaching the top of the inner scrollable.
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 300.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsNothing);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 0.0, visible: false);
|
||||||
|
|
||||||
|
// The snap animation should be triggered to expand the app bar
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -30.0)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
56.0,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true);
|
||||||
|
|
||||||
|
// Scroll away a bit more to trigger the snap close animation.
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 30.0)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Test Title'), findsNothing);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(find.byType(AppBar), findsNothing);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 0.0, visible: false);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('float expanded with pointer signal', (WidgetTester tester) async {
|
testWidgets('float expanded with pointer signal', (WidgetTester tester) async {
|
||||||
final GlobalKey appBarKey = GlobalKey();
|
final GlobalKey appBarKey = GlobalKey();
|
||||||
await tester.pumpWidget(buildFloatTest(
|
await tester.pumpWidget(buildFloatTest(
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
|
void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
|
||||||
@ -226,6 +228,230 @@ void main() {
|
|||||||
expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
|
expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
|
||||||
expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0));
|
expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Pointer scrolled floating', () {
|
||||||
|
Widget buildTest(Widget sliver) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
sliver,
|
||||||
|
SliverFixedExtentList(
|
||||||
|
itemExtent: 50.0,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(BuildContext context, int index) => Text('Item $index'),
|
||||||
|
childCount: 30,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifyGeometry({
|
||||||
|
required GlobalKey key,
|
||||||
|
required bool visible,
|
||||||
|
required double paintExtent
|
||||||
|
}) {
|
||||||
|
final RenderSliver target = key.currentContext!.findRenderObject()! as RenderSliver;
|
||||||
|
final SliverGeometry geometry = target.geometry!;
|
||||||
|
expect(geometry.visible, visible);
|
||||||
|
expect(geometry.paintExtent, paintExtent);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('SliverAppBar', (WidgetTester tester) async {
|
||||||
|
final GlobalKey appBarKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(SliverAppBar(
|
||||||
|
key: appBarKey,
|
||||||
|
floating: true,
|
||||||
|
title: const Text('Test Title'),
|
||||||
|
)));
|
||||||
|
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsOneWidget);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
56.0,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, visible: true, paintExtent: 56.0);
|
||||||
|
|
||||||
|
// Pointer scroll the app bar away, we will scroll back less to validate the
|
||||||
|
// app bar floats back in.
|
||||||
|
final Offset point1 = tester.getCenter(find.text('Item 5'));
|
||||||
|
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
||||||
|
testPointer.hover(point1);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 300.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsNothing);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 0.0, visible: false);
|
||||||
|
|
||||||
|
// Scroll back to float in appbar
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -50.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
56.0,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 50.0, visible: true);
|
||||||
|
|
||||||
|
// Float the rest of the way in.
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -250.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsOneWidget);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
56.0,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('SliverPersistentHeader', (WidgetTester tester) async {
|
||||||
|
final GlobalKey headerKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(SliverPersistentHeader(
|
||||||
|
key: headerKey,
|
||||||
|
floating: true,
|
||||||
|
delegate: HeaderDelegate(),
|
||||||
|
)));
|
||||||
|
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsOneWidget);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
verifyGeometry(key: headerKey, visible: true, paintExtent: 56.0);
|
||||||
|
|
||||||
|
// Pointer scroll the app bar away, we will scroll back less to validate the
|
||||||
|
// app bar floats back in.
|
||||||
|
final Offset point1 = tester.getCenter(find.text('Item 5'));
|
||||||
|
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
||||||
|
testPointer.hover(point1);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 300.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsNothing);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
verifyGeometry(key: headerKey, paintExtent: 0.0, visible: false);
|
||||||
|
|
||||||
|
// Scroll back to float in appbar
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -50.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
verifyGeometry(key: headerKey, paintExtent: 50.0, visible: true);
|
||||||
|
|
||||||
|
// Float the rest of the way in.
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -250.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsOneWidget);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
verifyGeometry(key: headerKey, paintExtent: 56.0, visible: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('and snapping SliverAppBar', (WidgetTester tester) async {
|
||||||
|
final GlobalKey appBarKey = GlobalKey();
|
||||||
|
await tester.pumpWidget(buildTest(SliverAppBar(
|
||||||
|
key: appBarKey,
|
||||||
|
floating: true,
|
||||||
|
snap: true,
|
||||||
|
title: const Text('Test Title'),
|
||||||
|
)));
|
||||||
|
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsOneWidget);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
56.0,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, visible: true, paintExtent: 56.0);
|
||||||
|
|
||||||
|
// Pointer scroll the app bar away, we will scroll back less to validate the
|
||||||
|
// app bar floats back in and then snaps to full size.
|
||||||
|
final Offset point1 = tester.getCenter(find.text('Item 5'));
|
||||||
|
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
||||||
|
testPointer.hover(point1);
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 300.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsNothing);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 0.0, visible: false);
|
||||||
|
|
||||||
|
// Scroll back to float in appbar
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -30.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
56.0,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 30.0, visible: true);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// The snap animation should have completed and the app bar should be
|
||||||
|
// fully expanded.
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
56.0,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 56.0, visible: true);
|
||||||
|
|
||||||
|
|
||||||
|
// Float back out a bit and trigger snap close animation.
|
||||||
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 50.0)));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Test Title'), findsOneWidget);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
|
||||||
|
56.0,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 6.0, visible: true);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// The snap animation should have completed and the app bar should no
|
||||||
|
// longer be visible.
|
||||||
|
expect(find.text('Test Title'), findsNothing);
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.byType(AppBar),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
verifyGeometry(key: appBarKey, paintExtent: 0.0, visible: false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class HeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||||
|
return Container(
|
||||||
|
height: 56,
|
||||||
|
color: Colors.red,
|
||||||
|
child: const Text('Test Title'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get maxExtent => 56;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get minExtent => 56;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestDelegate extends SliverPersistentHeaderDelegate {
|
class TestDelegate extends SliverPersistentHeaderDelegate {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user