flutter_flutter/packages/flutter/test/widgets/nested_scroll_view_test.dart
jslavitz cea4aa9b7b
Teach drag start behaviors to DragGestureRecognizer (#26246)
* the onStart callback will report the location of the pointer where it wins the gesture arena by default instead of the pointer down location. Fixes all tests related to changing this default value.
2019-01-09 10:53:47 -08:00

670 lines
31 KiB
Dart

// 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 'package:flutter/gestures.dart' show DragStartBehavior;
import '../rendering/mock_canvas.dart';
class _CustomPhysics extends ClampingScrollPhysics {
const _CustomPhysics({ ScrollPhysics parent }) : super(parent: parent);
@override
_CustomPhysics applyTo(ScrollPhysics ancestor) {
return _CustomPhysics(parent: buildParent(ancestor));
}
@override
Simulation createBallisticSimulation(ScrollMetrics position, double dragVelocity) {
return ScrollSpringSimulation(spring, 1000.0, 1000.0, 1000.0);
}
}
Widget buildTest({ ScrollController controller, String title ='TTTTTTTT' }) {
return Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scaffold(
drawerDragStartBehavior: DragStartBehavior.down,
body: DefaultTabController(
length: 4,
child: NestedScrollView(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text(title),
pinned: true,
expandedHeight: 200.0,
forceElevated: innerBoxIsScrolled,
bottom: const TabBar(
tabs: <Tab>[
Tab(text: 'AA'),
Tab(text: 'BB'),
Tab(text: 'CC'),
Tab(text: 'DD'),
],
),
),
];
},
body: TabBarView(
children: <Widget>[
ListView(
children: <Widget>[
Container(
height: 300.0,
child: const Text('aaa1'),
),
Container(
height: 200.0,
child: const Text('aaa2'),
),
Container(
height: 100.0,
child: const Text('aaa3'),
),
Container(
height: 50.0,
child: const Text('aaa4'),
),
],
),
ListView(
dragStartBehavior: DragStartBehavior.down,
children: <Widget>[
Container(
height: 100.0,
child: const Text('bbb1'),
),
],
),
Container(
child: const Center(child: Text('ccc1')),
),
ListView(
dragStartBehavior: DragStartBehavior.down,
children: <Widget>[
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 = 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 = TrackingScrollController();
expect(controller.mostRecentlyUpdatedPosition, isNull);
expect(controller.initialScrollOffset, 0.0);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: 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(Directionality(
textDirection: TextDirection.ltr,
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: MediaQuery(
data: const MediaQueryData(),
child: NestedScrollView(
physics: const _CustomPhysics(),
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
const SliverAppBar(
floating: true,
title: Text('AA'),
),
];
},
body: 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 {
debugDisableShadows = false;
const List<String> _tabs = <String>['Hello', 'World'];
int buildCount = 0;
await tester.pumpWidget(
MaterialApp(home: Material(child:
// THE FOLLOWING SECTION IS FROM THE NestedScrollView DOCUMENTATION
// (EXCEPT FOR THE CHANGES TO THE buildCount COUNTER)
DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
dragStartBehavior: DragStartBehavior.down,
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>[
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: 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: TabBar(
// These are the widgets to put in each tab in the tab bar.
tabs: _tabs.map<Widget>((String name) => Tab(text: name)).toList(),
dragStartBehavior: DragStartBehavior.down,
),
),
),
];
},
body: TabBarView(
dragStartBehavior: DragStartBehavior.down,
// These are the contents of the tab views, below the tabs.
children: _tabs.map<Widget>((String name) {
return SafeArea(
top: false,
bottom: false,
child: 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 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: PageStorageKey<String>(name),
dragStartBehavior: DragStartBehavior.down,
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber above.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
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: 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: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// This builder is called for each child.
// In this example, we just number each list item.
return ListTile(
title: 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()));
debugDisableShadows = true;
});
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 = ValueKey<int>(1);
const Key key2 = ValueKey<int>(2);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: DefaultTabController(
length: 1,
child: NestedScrollView(
dragStartBehavior: DragStartBehavior.down,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
const SliverPersistentHeader(
delegate: TestHeader(key: key1),
),
];
},
body: SingleChildScrollView(
dragStartBehavior: DragStartBehavior.down,
child: Container(
height: 1000.0,
child: const Placeholder(key: key2),
),
),
),
),
),
),
);
expect(tester.getRect(find.byKey(key1)), Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)), 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)), Rect.fromLTWH(0.0, -10.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)), 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)), Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)), 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)), 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)), 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)), 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)), 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 Placeholder(key: key);
}
@override
bool shouldRebuild(TestHeader oldDelegate) => false;
}