blob: a08e74275c2eed88dbd8d634d07e0d3733cd7160 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
class _CustomPhysics extends ClampingScrollPhysics {
const _CustomPhysics({ ScrollPhysics parent }) : super(parent: parent);
@override
_CustomPhysics applyTo(ScrollPhysics ancestor) {
return new _CustomPhysics(parent: buildParent(ancestor));
}
@override
Simulation createBallisticSimulation(ScrollMetrics position, double dragVelocity) {
return new ScrollSpringSimulation(spring, 1000.0, 1000.0, 1000.0);
}
}
Widget buildTest({ ScrollController controller, String title:'TTTTTTTT' }) {
return new Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new Scaffold(
body: new DefaultTabController(
length: 4,
child: new NestedScrollView(
controller: controller,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
title: new Text(title),
pinned: true,
expandedHeight: 200.0,
forceElevated: innerBoxIsScrolled,
bottom: const TabBar(
tabs: const <Tab>[
const Tab(text: 'AA'),
const Tab(text: 'BB'),
const Tab(text: 'CC'),
const Tab(text: 'DD'),
],
),
),
];
},
body: new TabBarView(
children: <Widget>[
new ListView(
children: <Widget>[
new Container(
height: 300.0,
child: const Text('aaa1'),
),
new Container(
height: 200.0,
child: const Text('aaa2'),
),
new Container(
height: 100.0,
child: const Text('aaa3'),
),
new Container(
height: 50.0,
child: const Text('aaa4'),
),
],
),
new ListView(
children: <Widget>[
new Container(
height: 100.0,
child: const Text('bbb1'),
),
],
),
new Container(
child: const Center(child: const Text('ccc1')),
),
new ListView(
children: <Widget>[
new Container(
height: 10000.0,
child: const Text('ddd1'),
),
],
),
],
),
),
),
),
),
),
);
}
void main() {
testWidgets('NestedScrollView overscroll and release and hold', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
await tester.pumpWidget(buildTest());
expect(find.text('aaa2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 250));
final Offset point1 = tester.getCenter(find.text('aaa1'));
await tester.dragFrom(point1, const Offset(0.0, 200.0));
await tester.pump();
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
await tester.flingFrom(point1, const Offset(0.0, -80.0), 50000.0);
await tester.pump(const Duration(milliseconds: 20));
final Offset point2 = tester.getCenter(find.text('aaa1'));
expect(point2.dy, greaterThan(point1.dy));
// TODO(ianh): Once we improve how we handle scrolling down from overscroll,
// the following expectation should switch to 200.0.
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 120.0);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('NestedScrollView overscroll and release and hold', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
await tester.pumpWidget(buildTest());
expect(find.text('aaa2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 250));
final Offset point = tester.getCenter(find.text('aaa1'));
await tester.flingFrom(point, const Offset(0.0, 200.0), 5000.0);
await tester.pump(const Duration(milliseconds: 10));
await tester.pump(const Duration(milliseconds: 10));
await tester.pump(const Duration(milliseconds: 10));
expect(find.text('aaa2'), findsNothing);
final TestGesture gesture1 = await tester.startGesture(point);
await tester.pump(const Duration(milliseconds: 5000));
expect(find.text('aaa2'), findsNothing);
await gesture1.moveBy(const Offset(0.0, 50.0));
await tester.pump(const Duration(milliseconds: 10));
await tester.pump(const Duration(milliseconds: 10));
expect(find.text('aaa2'), findsNothing);
await tester.pump(const Duration(milliseconds: 1000));
debugDefaultTargetPlatformOverride = null;
});
testWidgets('NestedScrollView overscroll and release', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
await tester.pumpWidget(buildTest());
expect(find.text('aaa2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 500));
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('aaa1')));
await gesture1.moveBy(const Offset(0.0, 200.0));
await tester.pumpAndSettle();
expect(find.text('aaa2'), findsNothing);
await tester.pump(const Duration(seconds: 1));
await gesture1.up();
await tester.pumpAndSettle();
expect(find.text('aaa2'), findsOneWidget);
debugDefaultTargetPlatformOverride = null;
}, skip: true); // https://github.com/flutter/flutter/issues/9040
testWidgets('NestedScrollView', (WidgetTester tester) async {
await tester.pumpWidget(buildTest());
expect(find.text('aaa2'), findsOneWidget);
expect(find.text('aaa3'), findsNothing);
expect(find.text('bbb1'), findsNothing);
await tester.pump(const Duration(milliseconds: 250));
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
await tester.drag(find.text('AA'), const Offset(0.0, -20.0));
await tester.pump(const Duration(milliseconds: 250));
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 180.0);
await tester.drag(find.text('AA'), const Offset(0.0, -20.0));
await tester.pump(const Duration(milliseconds: 250));
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 160.0);
await tester.drag(find.text('AA'), const Offset(0.0, -20.0));
await tester.pump(const Duration(milliseconds: 250));
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 140.0);
expect(find.text('aaa4'), findsNothing);
await tester.pump(const Duration(milliseconds: 250));
await tester.fling(find.text('AA'), const Offset(0.0, -50.0), 10000.0);
await tester.pumpAndSettle(const Duration(milliseconds: 250));
expect(find.text('aaa4'), findsOneWidget);
final double minHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
expect(minHeight, lessThan(140.0));
await tester.pump(const Duration(milliseconds: 250));
await tester.tap(find.text('BB'));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
expect(find.text('aaa4'), findsNothing);
expect(find.text('bbb1'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 250));
await tester.tap(find.text('CC'));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
expect(find.text('bbb1'), findsNothing);
expect(find.text('ccc1'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, minHeight);
await tester.pump(const Duration(milliseconds: 250));
await tester.fling(find.text('AA'), const Offset(0.0, 50.0), 10000.0);
await tester.pumpAndSettle(const Duration(milliseconds: 250));
expect(find.text('ccc1'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
});
testWidgets('NestedScrollView with a ScrollController', (WidgetTester tester) async {
final ScrollController controller = new ScrollController(initialScrollOffset: 50.0);
double scrollOffset;
controller.addListener(() {
scrollOffset = controller.offset;
});
await tester.pumpWidget(buildTest(controller: controller));
expect(controller.position.minScrollExtent, 0.0);
expect(controller.position.pixels, 50.0);
expect(controller.position.maxScrollExtent, 200.0);
// The appbar's expandedHeight - initialScrollOffset = 150.
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 150.0);
// Fully expand the appbar by scrolling (no animation) to 0.0.
controller.jumpTo(0.0);
await tester.pumpAndSettle();
expect(scrollOffset, 0.0);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
// Scroll back to 50.0 animating over 100ms.
controller.animateTo(50.0, duration: const Duration(milliseconds: 100), curve: Curves.linear);
await tester.pump();
await tester.pump();
expect(scrollOffset, 0.0);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
await tester.pump(const Duration(milliseconds: 50)); // 50ms - halfway to scroll offset = 50.0.
expect(scrollOffset, 25.0);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 175.0);
await tester.pump(const Duration(milliseconds: 50)); // 100ms - all the way to scroll offset = 50.0.
expect(scrollOffset, 50.0);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 150.0);
// Scroll to the end, (we're not scrolling to the end of the list that contains aaa1,
// just to the end of the outer scrollview). Verify that the first item in each tab
// is still visible.
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(scrollOffset, 200.0);
expect(find.text('aaa1'), findsOneWidget);
await tester.tap(find.text('BB'));
await tester.pumpAndSettle();
expect(find.text('bbb1'), findsOneWidget);
await tester.tap(find.text('CC'));
await tester.pumpAndSettle();
expect(find.text('ccc1'), findsOneWidget);
await tester.tap(find.text('DD'));
await tester.pumpAndSettle();
expect(find.text('ddd1'), findsOneWidget);
});
testWidgets('Three NestedScrollViews with one ScrollController', (WidgetTester tester) async {
final TrackingScrollController controller = new TrackingScrollController();
expect(controller.mostRecentlyUpdatedPosition, isNull);
expect(controller.initialScrollOffset, 0.0);
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new PageView(
children: <Widget>[
buildTest(controller: controller, title: 'Page0'),
buildTest(controller: controller, title: 'Page1'),
buildTest(controller: controller, title: 'Page2'),
],
),
));
// Initially Page0 is visible and Page0's appbar is fully expanded (height = 200.0).
expect(find.text('Page0'), findsOneWidget);
expect(find.text('Page1'), findsNothing);
expect(find.text('Page2'), findsNothing);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
// A scroll collapses Page0's appbar to 150.0.
controller.jumpTo(50.0);
await tester.pumpAndSettle();
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 150.0);
// Fling to Page1. Page1's appbar height is the same as the appbar for Page0.
await tester.fling(find.text('Page0'), const Offset(-100.0, 0.0), 10000.0);
await tester.pumpAndSettle();
expect(find.text('Page0'), findsNothing);
expect(find.text('Page1'), findsOneWidget);
expect(find.text('Page2'), findsNothing);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 150.0);
// Expand Page1's appbar and then fling to Page2. Page2's appbar appears
// fully expanded.
controller.jumpTo(0.0);
await tester.pumpAndSettle();
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
await tester.fling(find.text('Page1'), const Offset(-100.0, 0.0), 10000.0);
await tester.pumpAndSettle();
expect(find.text('Page0'), findsNothing);
expect(find.text('Page1'), findsNothing);
expect(find.text('Page2'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 200.0);
});
testWidgets('NestedScrollViews with custom physics', (WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new NestedScrollView(
physics: const _CustomPhysics(),
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
const SliverAppBar(
floating: true,
title: const Text('AA'),
),
];
},
body: new Container(),
),
),
));
expect(find.text('AA'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 500));
final Offset point1 = tester.getCenter(find.text('AA'));
await tester.dragFrom(point1, const Offset(0.0, 200.0));
await tester.pump(const Duration(milliseconds: 20));
final Offset point2 = tester.getCenter(find.text('AA', skipOffstage: false));
expect(point1.dy, greaterThan(point2.dy));
});
testWidgets('NestedScrollView and internal scrolling', (WidgetTester tester) async {
const List<String> _tabs = const <String>['Hello', 'World'];
int buildCount = 0;
await tester.pumpWidget(
new MaterialApp(home: new Material(child:
// THE FOLLOWING SECTION IS FROM THE NestedScrollView DOCUMENTATION
// (EXCEPT FOR THE CHANGES TO THE buildCount COUNTER)
new DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: new NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
buildCount += 1; // THIS LINE IS NOT IN THE ORIGINAL -- ADDED FOR TEST
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[
new SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: new SliverAppBar(
title: const Text('Books'), // This is the title in the app bar.
pinned: true,
expandedHeight: 150.0,
// The "forceElevated" property causes the SliverAppBar to show
// a shadow. The "innerBoxIsScrolled" parameter is true when the
// inner scroll view is scrolled beyond its "zero" point, i.e.
// when it appears to be scrolled below the SliverAppBar.
// Without this, there are cases where the shadow would appear
// or not appear inappropriately, because the SliverAppBar is
// not actually aware of the precise position of the inner
// scroll views.
forceElevated: innerBoxIsScrolled,
bottom: new TabBar(
// These are the widgets to put in each tab in the tab bar.
tabs: _tabs.map((String name) => new Tab(text: name)).toList(),
),
),
),
];
},
body: new TabBarView(
// These are the contents of the tab views, below the tabs.
children: _tabs.map((String name) {
return new SafeArea(
top: false,
bottom: false,
child: new Builder(
// This Builder is needed to provide a BuildContext that is "inside"
// the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
// find the NestedScrollView.
builder: (BuildContext context) {
return new CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: new PageStorageKey<String>(name),
slivers: <Widget>[
new SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber above.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
new SliverPadding(
padding: const EdgeInsets.all(8.0),
// In this example, the inner scroll view has
// fixed-height list items, hence the use of
// SliverFixedExtentList. However, one could use any
// sliver widget here, e.g. SliverList or SliverGrid.
sliver: new SliverFixedExtentList(
// The items in this example are fixed to 48 pixels
// high. This matches the Material Design spec for
// ListTile widgets.
itemExtent: 48.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
// This builder is called for each child.
// In this example, we just number each list item.
return new ListTile(
title: new Text('Item $index'),
);
},
// The childCount of the SliverChildBuilderDelegate
// specifies how many children this inner list
// has. In this example, each tab has a list of
// exactly 30 items, but this is arbitrary.
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
)
// END
)),
);
int expectedBuildCount = 0;
expectedBuildCount += 1;
expect(buildCount, expectedBuildCount);
expect(find.text('Item 2'), findsOneWidget);
expect(find.text('Item 18'), findsNothing);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
// scroll down
final TestGesture gesture0 = await tester.startGesture(tester.getCenter(find.text('Item 2')));
await gesture0.moveBy(const Offset(0.0, -120.0)); // tiny bit more than the pinned app bar height (56px * 2)
await tester.pump();
expect(buildCount, expectedBuildCount);
expect(find.text('Item 2'), findsOneWidget);
expect(find.text('Item 18'), findsNothing);
await gesture0.up();
await tester.pump(const Duration(milliseconds: 1)); // start shadow animation
expectedBuildCount += 1;
expect(buildCount, expectedBuildCount);
await tester.pump(const Duration(milliseconds: 1)); // during shadow animation
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
await tester.pump(const Duration(seconds: 1)); // end shadow animation
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
// scroll down
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('Item 2')));
await gesture1.moveBy(const Offset(0.0, -800.0));
await tester.pump();
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
expect(find.text('Item 2'), findsNothing);
expect(find.text('Item 18'), findsOneWidget);
await gesture1.up();
await tester.pump(const Duration(seconds: 1));
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
// swipe left to bring in tap on the right
final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
await gesture2.moveBy(const Offset(-400.0, 0.0));
await tester.pump();
expect(buildCount, expectedBuildCount);
expect(find.text('Item 18'), findsOneWidget);
expect(find.text('Item 2'), findsOneWidget);
expect(find.text('Item 0'), findsOneWidget);
expect(tester.getTopLeft(find.ancestor(of: find.text('Item 0'), matching: find.byType(ListTile))).dy,
tester.getBottomLeft(find.byType(AppBar)).dy + 8.0);
expect(find.byType(NestedScrollView), paints..shadow());
await gesture2.up();
await tester.pump(); // start sideways scroll
await tester.pump(const Duration(seconds: 1)); // end sideways scroll, triggers shadow going away
expect(buildCount, expectedBuildCount);
await tester.pump(const Duration(seconds: 1)); // start shadow going away
expectedBuildCount += 1;
expect(buildCount, expectedBuildCount);
await tester.pump(const Duration(seconds: 1)); // end shadow going away
expect(buildCount, expectedBuildCount);
expect(find.text('Item 18'), findsNothing);
expect(find.text('Item 2'), findsOneWidget);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
await tester.pump(const Duration(seconds: 1)); // just checking we don't rebuild...
expect(buildCount, expectedBuildCount);
// peek left to see it's still in the right place
final TestGesture gesture3 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
await gesture3.moveBy(const Offset(400.0, 0.0));
await tester.pump(); // bring the left page into view
expect(buildCount, expectedBuildCount);
await tester.pump(); // shadow comes back starting here
expectedBuildCount += 1;
expect(buildCount, expectedBuildCount);
expect(find.text('Item 18'), findsOneWidget);
expect(find.text('Item 2'), findsOneWidget);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
await tester.pump(const Duration(seconds: 1)); // shadow finishes coming back
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
await gesture3.moveBy(const Offset(-400.0, 0.0));
await gesture3.up();
await tester.pump(); // left tab view goes away
expect(buildCount, expectedBuildCount);
await tester.pump(); // shadow goes away starting here
expectedBuildCount += 1;
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
await tester.pump(const Duration(seconds: 1)); // shadow finishes going away
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
// scroll back up
final TestGesture gesture4 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
await gesture4.moveBy(const Offset(0.0, 200.0)); // expands the appbar again
await tester.pump();
expect(buildCount, expectedBuildCount);
expect(find.text('Item 2'), findsOneWidget);
expect(find.text('Item 18'), findsNothing);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
await gesture4.up();
await tester.pump(const Duration(seconds: 1));
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
// peek left to see it's now back at zero
final TestGesture gesture5 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
await gesture5.moveBy(const Offset(400.0, 0.0));
await tester.pump(); // bring the left page into view
await tester.pump(); // shadow would come back starting here, but there's no shadow to show
expect(buildCount, expectedBuildCount);
expect(find.text('Item 18'), findsNothing);
expect(find.text('Item 2'), findsNWidgets(2));
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
await tester.pump(const Duration(seconds: 1)); // shadow would be finished coming back
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
await gesture5.up();
await tester.pump(); // right tab view goes away
await tester.pumpAndSettle();
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
});
testWidgets('NestedScrollView and iOS bouncing', (WidgetTester tester) async {
// This verifies that overscroll bouncing works correctly on iOS. For
// example, this checks that if you pull to overscroll, friction is applied;
// it also makes sure that if you scroll back the other way, the scroll
// positions of the inner and outer list don't have a discontinuity.
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const Key key1 = const ValueKey<int>(1);
const Key key2 = const ValueKey<int>(2);
await tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new DefaultTabController(
length: 1,
child: new NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
const SliverPersistentHeader(
delegate: const TestHeader(key: key1),
),
];
},
body: new SingleChildScrollView(
child: new Container(
height: 1000.0,
child: const Placeholder(key: key2),
),
),
),
),
),
),
);
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)), new Rect.fromLTWH(0.0, 100.0, 800.0, 1000.0));
final TestGesture gesture = await tester.startGesture(const Offset(10.0, 10.0));
await gesture.moveBy(const Offset(0.0, -10.0)); // scroll up
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, -10.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)), new Rect.fromLTWH(0.0, 90.0, 800.0, 1000.0));
await gesture.moveBy(const Offset(0.0, 10.0)); // scroll back to origin
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)), new Rect.fromLTWH(0.0, 100.0, 800.0, 1000.0));
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)).top, greaterThan(100.0));
expect(tester.getRect(find.byKey(key2)).top, lessThan(130.0));
await gesture.moveBy(const Offset(0.0, -1.0)); // scroll back a little
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, -1.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)).top, greaterThan(100.0));
expect(tester.getRect(find.byKey(key2)).top, lessThan(129.0));
await gesture.moveBy(const Offset(0.0, -10.0)); // scroll back a lot
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, -11.0, 800.0, 100.0));
await gesture.moveBy(const Offset(0.0, 20.0)); // overscroll again
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
await gesture.up();
debugDefaultTargetPlatformOverride = null;
});
}
class TestHeader extends SliverPersistentHeaderDelegate {
const TestHeader({ this.key });
final Key key;
@override
double get minExtent => 100.0;
@override
double get maxExtent => 100.0;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Placeholder(key: key);
}
@override
bool shouldRebuild(TestHeader oldDelegate) => false;
}