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 {
|
||||
_SliverAppBarDelegate({
|
||||
required this.leading,
|
||||
@ -1264,7 +1212,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
systemOverlayStyle: systemOverlayStyle,
|
||||
),
|
||||
);
|
||||
return floating ? _FloatingAppBar(child: appBar) : appBar;
|
||||
return appBar;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -550,6 +550,10 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
|
||||
late Animation<double> _animation;
|
||||
double? _lastActualScrollOffset;
|
||||
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
|
||||
// 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.
|
||||
void maybeStartSnapAnimation(ScrollDirection direction) {
|
||||
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.
|
||||
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 (_effectiveScrollOffset! > maxExtent) // We're scrolled off-screen, but should reveal, so
|
||||
_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,
|
||||
);
|
||||
|
||||
// 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) {
|
||||
// Does not enter overscroll.
|
||||
_outerPosition!.applyClampedPointerSignalUpdate(delta);
|
||||
|
@ -216,6 +216,7 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc
|
||||
);
|
||||
final double oldPixels = pixels;
|
||||
forcePixels(targetPixels);
|
||||
isScrollingNotifier.value = true;
|
||||
didStartScroll();
|
||||
didUpdateScrollPositionBy(pixels - oldPixels);
|
||||
didEndScroll();
|
||||
|
@ -7,6 +7,8 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart' show TickerProvider;
|
||||
|
||||
import 'framework.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'scrollable.dart';
|
||||
|
||||
/// Delegate for configuring a [SliverPersistentHeader].
|
||||
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 {
|
||||
_SliverPersistentHeaderElement(_SliverPersistentHeaderRenderObjectWidget widget) : super(widget);
|
||||
_SliverPersistentHeaderElement(
|
||||
_SliverPersistentHeaderRenderObjectWidget widget, {
|
||||
this.floating = false,
|
||||
}) : assert(floating != null),
|
||||
super(widget);
|
||||
|
||||
final bool floating;
|
||||
|
||||
@override
|
||||
_SliverPersistentHeaderRenderObjectWidget get widget => super.widget as _SliverPersistentHeaderRenderObjectWidget;
|
||||
@ -229,11 +295,13 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
|
||||
owner!.buildScope(this, () {
|
||||
child = updateChild(
|
||||
child,
|
||||
widget.delegate.build(
|
||||
floating
|
||||
? _FloatingHeader(child: widget.delegate.build(
|
||||
this,
|
||||
shrinkOffset,
|
||||
overlapsContent,
|
||||
),
|
||||
overlapsContent
|
||||
))
|
||||
: widget.delegate.build(this, shrinkOffset, overlapsContent),
|
||||
null,
|
||||
);
|
||||
});
|
||||
@ -273,13 +341,16 @@ abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWid
|
||||
const _SliverPersistentHeaderRenderObjectWidget({
|
||||
Key? key,
|
||||
required this.delegate,
|
||||
this.floating = false,
|
||||
}) : assert(delegate != null),
|
||||
assert(floating != null),
|
||||
super(key: key);
|
||||
|
||||
final SliverPersistentHeaderDelegate delegate;
|
||||
final bool floating;
|
||||
|
||||
@override
|
||||
_SliverPersistentHeaderElement createElement() => _SliverPersistentHeaderElement(this);
|
||||
_SliverPersistentHeaderElement createElement() => _SliverPersistentHeaderElement(this, floating: floating);
|
||||
|
||||
@override
|
||||
_RenderSliverPersistentHeaderForWidgetsMixin createRenderObject(BuildContext context);
|
||||
@ -383,6 +454,7 @@ class _SliverFloatingPersistentHeader extends _SliverPersistentHeaderRenderObjec
|
||||
}) : super(
|
||||
key: key,
|
||||
delegate: delegate,
|
||||
floating: true,
|
||||
);
|
||||
|
||||
@override
|
||||
@ -428,6 +500,7 @@ class _SliverFloatingPinnedPersistentHeader extends _SliverPersistentHeaderRende
|
||||
}) : super(
|
||||
key: key,
|
||||
delegate: delegate,
|
||||
floating: true,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -1490,6 +1490,60 @@ void main() {
|
||||
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 {
|
||||
final GlobalKey appBarKey = GlobalKey();
|
||||
await tester.pumpWidget(buildFloatTest(
|
||||
|
@ -2,8 +2,10 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
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.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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user