Unverified Commit ff15d04f authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Add support for pointer scrolling to trigger floats & snaps (#76145)

parent 3cbfe82d
...@@ -1067,58 +1067,6 @@ class _AppBarState extends State<AppBar> { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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'; ...@@ -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 { ...@@ -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 { ...@@ -229,11 +295,13 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
owner!.buildScope(this, () { owner!.buildScope(this, () {
child = updateChild( child = updateChild(
child, child,
widget.delegate.build( floating
? _FloatingHeader(child: widget.delegate.build(
this, this,
shrinkOffset, shrinkOffset,
overlapsContent, overlapsContent
), ))
: widget.delegate.build(this, shrinkOffset, overlapsContent),
null, null,
); );
}); });
...@@ -273,13 +341,16 @@ abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWid ...@@ -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 ...@@ -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 ...@@ -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() { ...@@ -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 @@ ...@@ -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() { ...@@ -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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment