mirror of
https://github.com/alibaba/flutter-go.git
synced 2025-07-15 03:04:25 +08:00
775 lines
32 KiB
Dart
775 lines
32 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.
|
|
// Synced 2019-05-30T14:20:57.019972.
|
|
|
|
import 'package:flutter_web/foundation.dart';
|
|
import 'package:flutter_web/material.dart';
|
|
import 'package:flutter_web/rendering.dart';
|
|
import 'package:flutter_web_test/flutter_web_test.dart';
|
|
import 'package:flutter_web/gestures.dart' show DragStartBehavior;
|
|
|
|
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
|
|
)),
|
|
);
|
|
|
|
PhysicalModelLayer _dfsFindPhysicalLayer(ContainerLayer layer) {
|
|
expect(layer, isNotNull);
|
|
Layer child = layer.firstChild;
|
|
while (child != null) {
|
|
if (child is PhysicalModelLayer) {
|
|
return child;
|
|
}
|
|
if (child is ContainerLayer) {
|
|
final PhysicalModelLayer candidate = _dfsFindPhysicalLayer(child);
|
|
if (candidate != null) {
|
|
return candidate;
|
|
}
|
|
}
|
|
child = child.nextSibling;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
final ContainerLayer nestedScrollViewLayer =
|
|
find.byType(NestedScrollView).evaluate().first.renderObject.debugLayer;
|
|
void _checkPhysicalLayer({@required double elevation}) {
|
|
final PhysicalModelLayer layer =
|
|
_dfsFindPhysicalLayer(nestedScrollViewLayer);
|
|
expect(layer, isNotNull);
|
|
expect(layer.elevation, equals(elevation));
|
|
}
|
|
|
|
int expectedBuildCount = 0;
|
|
expectedBuildCount += 1;
|
|
expect(buildCount, expectedBuildCount);
|
|
expect(find.text('Item 2'), findsOneWidget);
|
|
expect(find.text('Item 18'), findsNothing);
|
|
_checkPhysicalLayer(elevation: 0);
|
|
// 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);
|
|
_checkPhysicalLayer(elevation: 0.00018262863159179688);
|
|
await tester.pump(const Duration(seconds: 1)); // end shadow animation
|
|
expect(buildCount, expectedBuildCount);
|
|
_checkPhysicalLayer(elevation: 4);
|
|
// 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);
|
|
_checkPhysicalLayer(elevation: 4);
|
|
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);
|
|
_checkPhysicalLayer(elevation: 4);
|
|
// 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);
|
|
_checkPhysicalLayer(elevation: 4);
|
|
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);
|
|
_checkPhysicalLayer(elevation: 0);
|
|
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);
|
|
_checkPhysicalLayer(elevation: 0);
|
|
await tester
|
|
.pump(const Duration(seconds: 1)); // shadow finishes coming back
|
|
expect(buildCount, expectedBuildCount);
|
|
_checkPhysicalLayer(elevation: 4);
|
|
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);
|
|
_checkPhysicalLayer(elevation: 4);
|
|
await tester.pump(const Duration(seconds: 1)); // shadow finishes going away
|
|
expect(buildCount, expectedBuildCount);
|
|
_checkPhysicalLayer(elevation: 0);
|
|
// 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);
|
|
_checkPhysicalLayer(elevation: 0);
|
|
await gesture4.up();
|
|
await tester.pump(const Duration(seconds: 1));
|
|
expect(buildCount, expectedBuildCount);
|
|
_checkPhysicalLayer(elevation: 0);
|
|
// 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));
|
|
_checkPhysicalLayer(elevation: 0);
|
|
await tester.pump(
|
|
const Duration(seconds: 1)); // shadow would be finished coming back
|
|
_checkPhysicalLayer(elevation: 0);
|
|
await gesture5.up();
|
|
await tester.pump(); // right tab view goes away
|
|
await tester.pumpAndSettle();
|
|
expect(buildCount, expectedBuildCount);
|
|
_checkPhysicalLayer(elevation: 0);
|
|
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)),
|
|
const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
|
|
expect(tester.getRect(find.byKey(key2)),
|
|
const 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)),
|
|
const Rect.fromLTWH(0.0, -10.0, 800.0, 100.0));
|
|
expect(tester.getRect(find.byKey(key2)),
|
|
const 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)),
|
|
const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
|
|
expect(tester.getRect(find.byKey(key2)),
|
|
const 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)),
|
|
const 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)),
|
|
const 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)),
|
|
const 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)),
|
|
const 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;
|
|
}
|