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

Exposing inner controller of NestedScrollView (#49004)

parent 2dc71a34
......@@ -120,9 +120,10 @@ typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContex
/// 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.
/// // 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
......@@ -136,7 +137,8 @@ typedef NestedScrollViewHeaderSliversBuilder = List<Widget> Function(BuildContex
/// key: PageStorageKey<String>(name),
/// slivers: <Widget>[
/// SliverOverlapInjector(
/// // This is the flip side of the SliverOverlapAbsorber above.
/// // This is the flip side of the SliverOverlapAbsorber
/// // above.
/// handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
/// ),
/// SliverPadding(
......@@ -268,7 +270,10 @@ class NestedScrollView extends StatefulWidget {
/// documentation.
static SliverOverlapAbsorberHandle sliverOverlapAbsorberHandleFor(BuildContext context) {
final _InheritedNestedScrollView target = context.dependOnInheritedWidgetOfExactType<_InheritedNestedScrollView>();
assert(target != null, 'NestedScrollView.sliverOverlapAbsorberHandleFor must be called with a context that contains a NestedScrollView.');
assert(
target != null,
'NestedScrollView.sliverOverlapAbsorberHandleFor must be called with a context that contains a NestedScrollView.',
);
return target.state._absorberHandle;
}
......@@ -285,18 +290,93 @@ class NestedScrollView extends StatefulWidget {
}
@override
_NestedScrollViewState createState() => _NestedScrollViewState();
NestedScrollViewState createState() => NestedScrollViewState();
}
class _NestedScrollViewState extends State<NestedScrollView> {
/// The [State] for a [NestedScrollView].
///
/// The [ScrollController]s, [innerController] and [outerController], of the
/// [NestedScrollView]'s children may be accessed through its state. This is
/// useful for obtaining respective scroll positions in the [NestedScrollView].
///
/// If you want to access the inner or outer scroll controller of a
/// [NestedScrollView], you can get its [NestedScrollViewState] by supplying a
/// `GlobalKey<NestedScrollViewState>` to the [NestedScrollView.key] parameter).
///
/// {@tool sample --template=stateless_widget_material}
/// [NestedScrollViewState] can be obtained using a [GlobalKey].
/// Using the following setup, you can access the inner scroll controller
/// using `globalKey.currentState.innerController`.
///
/// ```dart preamble
/// final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
/// ```
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// return NestedScrollView(
/// key: globalKey,
/// headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
/// return <Widget>[
/// SliverAppBar(
/// title: Text('NestedScrollViewState Demo!'),
/// ),
/// ];
/// },
/// body: CustomScrollView(
/// // Body slivers go here!
/// ),
/// );
/// }
///
/// ScrollController get innerController {
/// return globalKey.currentState.innerController;
/// }
/// ```
/// {@end-tool}
class NestedScrollViewState extends State<NestedScrollView> {
final SliverOverlapAbsorberHandle _absorberHandle = SliverOverlapAbsorberHandle();
/// The [ScrollController] provided to the [ScrollView] in
/// [NestedScrollView.body].
///
/// Manipulating the [ScrollPosition] of this controller pushes the outer
/// header sliver(s) up and out of view. The position of the [outerController]
/// will be set to [ScrollPosition.maxScrollExtent], unless you use
/// [ScrollPosition.setPixels].
///
/// See also:
///
/// * [outerController], which exposes the [ScrollController] used by the
/// the sliver(s) contained in [NestedScrollView.headerSliverBuilder].
ScrollController get innerController => _coordinator._innerController;
/// The [ScrollController] provided to the [ScrollView] in
/// [NestedScrollView.headerSliverBuilder].
///
/// This is equivalent to [NestedScrollView.controller], if provided.
///
/// Manipulating the [ScrollPosition] of this controller pushes the inner body
/// sliver(s) down. The position of the [innerController] will be set to
/// [ScrollPosition.minScrollExtent], unless you use
/// [ScrollPosition.setPixels]. Visually, the inner body will be scrolled to
/// its beginning.
///
/// See also:
///
/// * [innerController], which exposes the [ScrollController] used by the
/// [ScrollView] contained in [NestedScrollView.body].
ScrollController get outerController => _coordinator._outerController;
_NestedScrollCoordinator _coordinator;
@override
void initState() {
super.initState();
_coordinator = _NestedScrollCoordinator(this, widget.controller, _handleHasScrolledBodyChanged);
_coordinator = _NestedScrollCoordinator(
this, widget.controller,
_handleHasScrolledBodyChanged,
);
}
@override
......@@ -348,8 +428,8 @@ class _NestedScrollViewState extends State<NestedScrollView> {
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
physics: widget.physics != null
? widget.physics.applyTo(const ClampingScrollPhysics())
: const ClampingScrollPhysics(),
? widget.physics.applyTo(const ClampingScrollPhysics())
: const ClampingScrollPhysics(),
controller: _coordinator._outerController,
slivers: widget._buildSlivers(
context,
......@@ -410,7 +490,7 @@ class _InheritedNestedScrollView extends InheritedWidget {
assert(child != null),
super(key: key, child: child);
final _NestedScrollViewState state;
final NestedScrollViewState state;
@override
bool updateShouldNotify(_InheritedNestedScrollView old) => state != old.state;
......@@ -469,11 +549,19 @@ typedef _NestedScrollActivityGetter = ScrollActivity Function(_NestedScrollPosit
class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldController {
_NestedScrollCoordinator(this._state, this._parent, this._onHasScrolledBodyChanged) {
final double initialScrollOffset = _parent?.initialScrollOffset ?? 0.0;
_outerController = _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'outer');
_innerController = _NestedScrollController(this, initialScrollOffset: 0.0, debugLabel: 'inner');
_outerController = _NestedScrollController(
this,
initialScrollOffset: initialScrollOffset,
debugLabel: 'outer',
);
_innerController = _NestedScrollController(
this,
initialScrollOffset: 0.0,
debugLabel: 'inner',
);
}
final _NestedScrollViewState _state;
final NestedScrollViewState _state;
ScrollController _parent;
final VoidCallback _onHasScrolledBodyChanged;
......@@ -550,14 +638,22 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
@override
void goIdle() {
beginActivity(_createIdleScrollActivity(_outerPosition), _createIdleScrollActivity);
beginActivity(
_createIdleScrollActivity(_outerPosition),
_createIdleScrollActivity,
);
}
@override
void goBallistic(double velocity) {
beginActivity(
createOuterBallisticScrollActivity(velocity),
(_NestedScrollPosition position) => createInnerBallisticScrollActivity(position, velocity),
(_NestedScrollPosition position) {
return createInnerBallisticScrollActivity(
position,
velocity,
);
},
);
}
......@@ -593,7 +689,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
if (innerPosition == null) {
// It's either just us or a velocity=0 situation.
return _outerPosition.createBallisticScrollActivity(
_outerPosition.physics.createBallisticSimulation(_outerPosition, velocity),
_outerPosition.physics.createBallisticSimulation(
_outerPosition,
velocity,
),
mode: _NestedBallisticScrollActivityMode.independent,
);
}
......@@ -611,7 +710,9 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) {
return position.createBallisticScrollActivity(
position.physics.createBallisticSimulation(
velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity),
velocity == 0
? position as ScrollMetrics
: _getMetrics(position, velocity),
velocity,
),
mode: _NestedBallisticScrollActivityMode.inner,
......@@ -622,7 +723,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
assert(innerPosition != null);
double pixels, minRange, maxRange, correctionOffset, extra;
if (innerPosition.pixels == innerPosition.minScrollExtent) {
pixels = _outerPosition.pixels.clamp(_outerPosition.minScrollExtent, _outerPosition.maxScrollExtent) as double; // TODO(ianh): gracefully handle out-of-range outer positions
pixels = _outerPosition.pixels.clamp(
_outerPosition.minScrollExtent,
_outerPosition.maxScrollExtent,
) as double; // TODO(ianh): gracefully handle out-of-range outer positions
minRange = _outerPosition.minScrollExtent;
maxRange = _outerPosition.maxScrollExtent;
assert(minRange <= maxRange);
......@@ -688,7 +792,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
double unnestOffset(double value, _NestedScrollPosition source) {
if (source == _outerPosition)
return value.clamp(_outerPosition.minScrollExtent, _outerPosition.maxScrollExtent) as double;
return value.clamp(
_outerPosition.minScrollExtent,
_outerPosition.maxScrollExtent,
) as double;
if (value < source.minScrollExtent)
return value - source.minScrollExtent + _outerPosition.minScrollExtent;
return value - source.minScrollExtent + _outerPosition.maxScrollExtent;
......@@ -696,7 +803,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
double nestOffset(double value, _NestedScrollPosition target) {
if (target == _outerPosition)
return value.clamp(_outerPosition.minScrollExtent, _outerPosition.maxScrollExtent) as double;
return value.clamp(
_outerPosition.minScrollExtent,
_outerPosition.maxScrollExtent,
) as double;
if (value < _outerPosition.minScrollExtent)
return value - _outerPosition.minScrollExtent + target.minScrollExtent;
if (value > _outerPosition.maxScrollExtent)
......@@ -711,7 +821,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
for (final _NestedScrollPosition position in _innerPositions) {
if (!position.haveDimensions)
return;
maxInnerExtent = math.max(maxInnerExtent, position.maxScrollExtent - position.minScrollExtent);
maxInnerExtent = math.max(
maxInnerExtent,
position.maxScrollExtent - position.minScrollExtent,
);
}
_outerPosition.updateCanDrag(maxInnerExtent);
}
......@@ -758,7 +871,10 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
ScrollHoldController hold(VoidCallback holdCancelCallback) {
beginActivity(
HoldScrollActivity(delegate: _outerPosition, onHoldCanceled: holdCancelCallback),
HoldScrollActivity(
delegate: _outerPosition,
onHoldCanceled: holdCancelCallback,
),
(_NestedScrollPosition position) => HoldScrollActivity(delegate: position),
);
return this;
......@@ -786,7 +902,9 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
@override
void applyUserOffset(double delta) {
updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
updateUserScrollDirection(
delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse
);
assert(delta != 0.0);
if (_innerPositions.isEmpty) {
_outerPosition.applyFullDragUpdate(delta);
......@@ -805,7 +923,8 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
}
} else {
// dragging "down" - delta is positive
// prioritize the inner views, so that the inner content will move before the app bar grows
// prioritize the inner views, so that the inner content will move before
// the app bar grows
double outerDelta = 0.0; // it will go positive if it changes
final List<double> overscrolls = <double>[];
final List<_NestedScrollPosition> innerPositions = _innerPositions.toList();
......@@ -831,7 +950,9 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont
}
void updateParent() {
_outerPosition?.setParent(_parent ?? PrimaryScrollController.of(_state.context));
_outerPosition?.setParent(
_parent ?? PrimaryScrollController.of(_state.context)
);
}
@mustCallSuper
......@@ -984,9 +1105,13 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
// One is if the physics allow it, via applyFullDragUpdate (see below). An
// overscroll situation can also be forced, e.g. if the scroll position is
// artificially set using the scroll controller.
final double min = delta < 0.0 ? -double.infinity : math.min(minScrollExtent, pixels);
final double min = delta < 0.0
? -double.infinity
: math.min(minScrollExtent, pixels);
// The logic for max is equivalent but on the other side.
final double max = delta > 0.0 ? double.infinity : math.max(maxScrollExtent, pixels);
final double max = delta > 0.0
? double.infinity
: math.max(maxScrollExtent, pixels);
final double oldPixels = pixels;
final double newPixels = (pixels - delta).clamp(min, max) as double;
final double clampedDelta = newPixels - pixels;
......@@ -1007,7 +1132,10 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
assert(delta != 0.0);
final double oldPixels = pixels;
// Apply friction:
final double newPixels = pixels - physics.applyPhysicsToUserOffset(this, delta);
final double newPixels = pixels - physics.applyPhysicsToUserOffset(
this,
delta,
);
if (oldPixels == newPixels)
return 0.0; // delta must have been so small we dropped it during floating point addition
// Check for overscroll:
......@@ -1050,7 +1178,8 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
beginActivity(IdleScrollActivity(this));
}
// This is called by activities when they finish their work and want to go ballistic.
// This is called by activities when they finish their work and want to go
// ballistic.
@override
void goBallistic(double velocity) {
Simulation simulation;
......@@ -1075,9 +1204,20 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
assert(metrics != null);
if (metrics.minRange == metrics.maxRange)
return IdleScrollActivity(this);
return _NestedOuterBallisticScrollActivity(coordinator, this, metrics, simulation, context.vsync);
return _NestedOuterBallisticScrollActivity(
coordinator,
this,
metrics,
simulation,
context.vsync,
);
case _NestedBallisticScrollActivityMode.inner:
return _NestedInnerBallisticScrollActivity(coordinator, this, simulation, context.vsync);
return _NestedInnerBallisticScrollActivity(
coordinator,
this,
simulation,
context.vsync,
);
case _NestedBallisticScrollActivityMode.independent:
return BallisticScrollActivity(this, simulation, context.vsync);
}
......@@ -1090,7 +1230,11 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
@required Duration duration,
@required Curve curve,
}) {
return coordinator.animateTo(coordinator.unnestOffset(to, this), duration: duration, curve: curve);
return coordinator.animateTo(
coordinator.unnestOffset(to, this),
duration: duration,
curve: curve,
);
}
@override
......@@ -1157,12 +1301,18 @@ class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity {
@override
void resetActivity() {
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(
delegate,
velocity,
));
}
@override
void applyNewDimensions() {
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(
delegate,
velocity,
));
}
@override
......@@ -1190,12 +1340,16 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
@override
void resetActivity() {
delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
delegate.beginActivity(
coordinator.createOuterBallisticScrollActivity(velocity)
);
}
@override
void applyNewDimensions() {
delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
delegate.beginActivity(
coordinator.createOuterBallisticScrollActivity(velocity)
);
}
@override
......@@ -1290,7 +1444,10 @@ class SliverOverlapAbsorberHandle extends ChangeNotifier {
double _scrollExtent;
void _setExtents(double layoutValue, double scrollValue) {
assert(_writers == 1, 'Multiple RenderSliverOverlapAbsorbers have been provided the same SliverOverlapAbsorberHandle.');
assert(
_writers == 1,
'Multiple RenderSliverOverlapAbsorbers have been provided the same SliverOverlapAbsorberHandle.',
);
_layoutExtent = layoutValue;
_scrollExtent = scrollValue;
}
......@@ -1434,7 +1591,10 @@ class RenderSliverOverlapAbsorber extends RenderSliver with RenderObjectWithChil
@override
void performLayout() {
assert(handle._writers == 1, 'A SliverOverlapAbsorberHandle cannot be passed to multiple RenderSliverOverlapAbsorber objects at the same time.');
assert(
handle._writers == 1,
'A SliverOverlapAbsorberHandle cannot be passed to multiple RenderSliverOverlapAbsorber objects at the same time.',
);
if (child == null) {
geometry = const SliverGeometry();
return;
......@@ -1453,7 +1613,10 @@ class RenderSliverOverlapAbsorber extends RenderSliver with RenderObjectWithChil
hasVisualOverflow: childLayoutGeometry.hasVisualOverflow,
scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection,
);
handle._setExtents(childLayoutGeometry.maxScrollObstructionExtent, childLayoutGeometry.maxScrollObstructionExtent);
handle._setExtents(
childLayoutGeometry.maxScrollObstructionExtent,
childLayoutGeometry.maxScrollObstructionExtent,
);
}
@override
......@@ -1464,7 +1627,11 @@ class RenderSliverOverlapAbsorber extends RenderSliver with RenderObjectWithChil
@override
bool hitTestChildren(SliverHitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
if (child != null)
return child.hitTest(result, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition);
return child.hitTest(
result,
mainAxisPosition: mainAxisPosition,
crossAxisPosition: crossAxisPosition,
);
return false;
}
......@@ -1599,7 +1766,10 @@ class RenderSliverOverlapInjector extends RenderSliver {
void performLayout() {
_currentLayoutExtent = handle.layoutExtent;
_currentMaxExtent = handle.layoutExtent;
final double clampedLayoutExtent = math.min(_currentLayoutExtent - constraints.scrollOffset, constraints.remainingPaintExtent);
final double clampedLayoutExtent = math.min(
_currentLayoutExtent - constraints.scrollOffset,
constraints.remainingPaintExtent,
);
geometry = SliverGeometry(
scrollExtent: _currentLayoutExtent,
paintExtent: math.max(0.0, clampedLayoutExtent),
......@@ -1631,7 +1801,14 @@ class RenderSliverOverlapInjector extends RenderSliver {
break;
}
for (int index = -2; index <= 2; index += 1) {
paintZigZag(context.canvas, paint, start - delta * index.toDouble(), end - delta * index.toDouble(), 10, 10.0);
paintZigZag(
context.canvas,
paint,
start - delta * index.toDouble(),
end - delta * index.toDouble(),
10,
10.0,
);
}
}
return true;
......@@ -1680,7 +1857,10 @@ class NestedScrollViewViewport extends Viewport {
RenderNestedScrollViewViewport createRenderObject(BuildContext context) {
return RenderNestedScrollViewViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(
context,
axisDirection,
),
anchor: anchor,
offset: offset,
handle: handle,
......@@ -1691,7 +1871,10 @@ class NestedScrollViewViewport extends Viewport {
void updateRenderObject(BuildContext context, RenderNestedScrollViewViewport renderObject) {
renderObject
..axisDirection = axisDirection
..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(
context,
axisDirection,
)
..anchor = anchor
..offset = offset
..handle = handle;
......@@ -1709,7 +1892,8 @@ class NestedScrollViewViewport extends Viewport {
/// This viewport takes a [SliverOverlapAbsorberHandle] and notifies it any time
/// the viewport needs to recompute its layout (e.g. when it is scrolled).
class RenderNestedScrollViewViewport extends RenderViewport {
/// Create a variant of [RenderViewport] that has a [SliverOverlapAbsorberHandle].
/// Create a variant of [RenderViewport] that has a
/// [SliverOverlapAbsorberHandle].
///
/// The [handle] must not be null.
RenderNestedScrollViewViewport({
......
......@@ -22,7 +22,12 @@ class _CustomPhysics extends ClampingScrollPhysics {
}
}
Widget buildTest({ ScrollController controller, String title = 'TTTTTTTT' }) {
Widget buildTest({
ScrollController controller,
String title = 'TTTTTTTT',
Key key,
bool expanded = true,
}) {
return Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
......@@ -38,6 +43,7 @@ Widget buildTest({ ScrollController controller, String title = 'TTTTTTTT' }) {
body: DefaultTabController(
length: 4,
child: NestedScrollView(
key: key,
dragStartBehavior: DragStartBehavior.down,
controller: controller,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
......@@ -45,7 +51,7 @@ Widget buildTest({ ScrollController controller, String title = 'TTTTTTTT' }) {
SliverAppBar(
title: Text(title),
pinned: true,
expandedHeight: 200.0,
expandedHeight: expanded ? 200.0 : 0.0,
forceElevated: innerBoxIsScrolled,
bottom: const TabBar(
tabs: <Tab>[
......@@ -119,7 +125,10 @@ void main() {
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);
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'));
......@@ -128,6 +137,7 @@ void main() {
// the following expectation should switch to 200.0.
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 120.0);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('NestedScrollView overscroll and release and hold', (WidgetTester tester) async {
await tester.pumpWidget(buildTest());
expect(find.text('aaa2'), findsOneWidget);
......@@ -147,11 +157,14 @@ void main() {
expect(find.text('aaa2'), findsNothing);
await tester.pump(const Duration(milliseconds: 1000));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('NestedScrollView overscroll and release', (WidgetTester tester) async {
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')));
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);
......@@ -169,19 +182,31 @@ void main() {
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);
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);
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);
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(
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
140.0,
);
expect(find.text('aaa4'), findsNothing);
await tester.pump(const Duration(milliseconds: 250));
......@@ -203,17 +228,25 @@ void main() {
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);
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);
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);
final ScrollController controller = ScrollController(
initialScrollOffset: 50.0,
);
double scrollOffset;
controller.addListener(() {
......@@ -226,26 +259,45 @@ void main() {
expect(controller.position.maxScrollExtent, 200.0);
// The appbar's expandedHeight - initialScrollOffset = 150.
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 150.0);
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);
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);
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);
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);
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);
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
......@@ -288,12 +340,18 @@ void main() {
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);
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);
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);
......@@ -301,19 +359,28 @@ void main() {
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);
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);
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);
expect(
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
200.0,
);
});
testWidgets('NestedScrollViews with custom physics', (WidgetTester tester) async {
......@@ -347,7 +414,10 @@ void main() {
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));
final Offset point2 = tester.getCenter(find.text(
'AA',
skipOffstage: false,
));
expect(point1.dy, greaterThan(point2.dy));
});
......@@ -368,29 +438,31 @@ void main() {
// 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.
// 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),
sliver: 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.
// 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.
// 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,
),
......@@ -406,24 +478,27 @@ void main() {
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.
// 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.
// 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.
// This is the flip side of the
// SliverOverlapAbsorber above.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
......@@ -431,24 +506,27 @@ void main() {
// 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 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.
// 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.
// 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.
// 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,
),
),
......@@ -498,7 +576,9 @@ void main() {
expect(find.text('Item 18'), findsNothing);
_checkPhysicalLayer(elevation: 0);
// scroll down
final TestGesture gesture0 = await tester.startGesture(tester.getCenter(find.text('Item 2')));
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);
......@@ -515,7 +595,9 @@ void main() {
expect(buildCount, expectedBuildCount);
_checkPhysicalLayer(elevation: 4);
// scroll down
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('Item 2')));
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);
......@@ -527,15 +609,22 @@ void main() {
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)));
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(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
......@@ -552,7 +641,9 @@ void main() {
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)));
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);
......@@ -577,7 +668,9 @@ void main() {
expect(buildCount, expectedBuildCount);
_checkPhysicalLayer(elevation: 0);
// scroll back up
final TestGesture gesture4 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
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);
......@@ -589,7 +682,9 @@ void main() {
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)));
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
......@@ -640,39 +735,467 @@ void main() {
),
),
);
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));
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));
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));
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(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(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));
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));
expect(
tester.getRect(find.byKey(key1)),
const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0),
);
await gesture.up();
debugDefaultTargetPlatformOverride = null;
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
group('NestedScrollViewState exposes inner and outer controllers', () {
testWidgets('Scrolling by less than the outer extent does not scroll the inner body', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey,
expanded: false,
));
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
expect(appBarHeight, 104.0);
final double scrollExtent = appBarHeight - 50.0;
expect(globalKey.currentState.outerController.offset, 0.0);
expect(globalKey.currentState.innerController.offset, 0.0);
// The scroll gesture should occur in the inner body, so the whole
// scroll view is scrolled.
final TestGesture gesture = await tester.startGesture(Offset(
0.0,
appBarHeight + 1.0,
));
await gesture.moveBy(Offset(0.0, -scrollExtent));
await tester.pump();
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// This is not an expanded AppBar.
expect(appBarHeight, 104.0);
// The outer scroll controller should show an offset of the applied
// scrollExtent.
expect(globalKey.currentState.outerController.offset, 54.0);
// the inner scroll controller should not have scrolled.
expect(globalKey.currentState.innerController.offset, 0.0);
});
testWidgets('Scrolling by exactly the outer extent does not scroll the inner body', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey,
expanded: false,
));
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
expect(appBarHeight, 104.0);
final double scrollExtent = appBarHeight;
expect(globalKey.currentState.outerController.offset, 0.0);
expect(globalKey.currentState.innerController.offset, 0.0);
// The scroll gesture should occur in the inner body, so the whole
// scroll view is scrolled.
final TestGesture gesture = await tester.startGesture(Offset(
0.0,
appBarHeight + 1.0,
));
await gesture.moveBy(Offset(0.0, -scrollExtent));
await tester.pump();
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// This is not an expanded AppBar.
expect(appBarHeight, 104.0);
// The outer scroll controller should show an offset of the applied
// scrollExtent.
expect(globalKey.currentState.outerController.offset, 104.0);
// the inner scroll controller should not have scrolled.
expect(globalKey.currentState.innerController.offset, 0.0);
});
testWidgets('Scrolling by greater than the outer extent scrolls the inner body', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey,
expanded: false,
));
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
expect(appBarHeight, 104.0);
final double scrollExtent = appBarHeight + 50.0;
expect(globalKey.currentState.outerController.offset, 0.0);
expect(globalKey.currentState.innerController.offset, 0.0);
// The scroll gesture should occur in the inner body, so the whole
// scroll view is scrolled.
final TestGesture gesture = await tester.startGesture(Offset(
0.0,
appBarHeight + 1.0,
));
await gesture.moveBy(Offset(0.0, -scrollExtent));
await tester.pump();
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// This is not an expanded AppBar.
expect(appBarHeight, 104.0);
// The outer scroll controller should show an offset of the applied
// scrollExtent.
expect(globalKey.currentState.outerController.offset, appBarHeight);
// the inner scroll controller should have scrolled equivalent to the
// difference between the applied scrollExtent and the outer extent.
expect(
globalKey.currentState.innerController.offset,
scrollExtent - appBarHeight,
);
});
testWidgets('scrolling by less than the expanded outer extent does not scroll the inner body', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
await tester.pumpWidget(buildTest(key: globalKey));
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
expect(appBarHeight, 200.0);
final double scrollExtent = appBarHeight - 50.0;
expect(globalKey.currentState.outerController.offset, 0.0);
expect(globalKey.currentState.innerController.offset, 0.0);
// The scroll gesture should occur in the inner body, so the whole
// scroll view is scrolled.
final TestGesture gesture = await tester.startGesture(Offset(
0.0,
appBarHeight + 1.0,
));
await gesture.moveBy(Offset(0.0, -scrollExtent));
await tester.pump();
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// This is an expanding AppBar.
expect(appBarHeight, 104.0);
// The outer scroll controller should show an offset of the applied
// scrollExtent.
expect(globalKey.currentState.outerController.offset, 150.0);
// the inner scroll controller should not have scrolled.
expect(globalKey.currentState.innerController.offset, 0.0);
});
testWidgets('scrolling by exactly the expanded outer extent does not scroll the inner body', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
await tester.pumpWidget(buildTest(key: globalKey));
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
expect(appBarHeight, 200.0);
final double scrollExtent = appBarHeight;
expect(globalKey.currentState.outerController.offset, 0.0);
expect(globalKey.currentState.innerController.offset, 0.0);
// The scroll gesture should occur in the inner body, so the whole
// scroll view is scrolled.
final TestGesture gesture = await tester.startGesture(Offset(
0.0,
appBarHeight + 1.0,
));
await gesture.moveBy(Offset(0.0, -scrollExtent));
await tester.pump();
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// This is an expanding AppBar.
expect(appBarHeight, 104.0);
// The outer scroll controller should show an offset of the applied
// scrollExtent.
expect(globalKey.currentState.outerController.offset, 200.0);
// the inner scroll controller should not have scrolled.
expect(globalKey.currentState.innerController.offset, 0.0);
});
testWidgets('scrolling by greater than the expanded outer extent scrolls the inner body', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
await tester.pumpWidget(buildTest(key: globalKey));
double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
expect(appBarHeight, 200.0);
final double scrollExtent = appBarHeight + 50.0;
expect(globalKey.currentState.outerController.offset, 0.0);
expect(globalKey.currentState.innerController.offset, 0.0);
// The scroll gesture should occur in the inner body, so the whole
// scroll view is scrolled.
final TestGesture gesture = await tester.startGesture(Offset(
0.0,
appBarHeight + 1.0,
));
await gesture.moveBy(Offset(0.0, -scrollExtent));
await tester.pump();
appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// This is an expanding AppBar.
expect(appBarHeight, 104.0);
// The outer scroll controller should show an offset of the applied
// scrollExtent.
expect(globalKey.currentState.outerController.offset, 200.0);
// the inner scroll controller should have scrolled equivalent to the
// difference between the applied scrollExtent and the outer extent.
expect(globalKey.currentState.innerController.offset, 50.0);
});
testWidgets('NestedScrollViewState.outerController should correspond to NestedScrollView.controller', (
WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey = GlobalKey();
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(buildTest(
controller: scrollController,
key: globalKey,
));
// Scroll to compare offsets between controllers.
final TestGesture gesture = await tester.startGesture(const Offset(
0.0,
100.0,
));
await gesture.moveBy(const Offset(0.0, -100.0));
await tester.pump();
expect(
scrollController.offset,
globalKey.currentState.outerController.offset,
);
expect(
tester.widget<NestedScrollView>(find.byType(NestedScrollView)).controller.offset,
globalKey.currentState.outerController.offset,
);
});
group('manipulating controllers when', () {
testWidgets('outer: not scrolled, inner: not scrolled', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey1,
expanded: false,
));
expect(globalKey1.currentState.outerController.position.pixels, 0.0);
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
final double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// Manipulating Inner
globalKey1.currentState.innerController.jumpTo(100.0);
expect(globalKey1.currentState.innerController.position.pixels, 100.0);
expect(
globalKey1.currentState.outerController.position.pixels,
appBarHeight,
);
globalKey1.currentState.innerController.jumpTo(0.0);
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
expect(
globalKey1.currentState.outerController.position.pixels,
appBarHeight,
);
// Reset
final GlobalKey<NestedScrollViewState> globalKey2 = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey2,
expanded: false,
));
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
// Manipulating Outer
globalKey2.currentState.outerController.jumpTo(100.0);
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
expect(globalKey2.currentState.outerController.position.pixels, 100.0);
globalKey2.currentState.outerController.jumpTo(0.0);
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
});
testWidgets('outer: not scrolled, inner: scrolled', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey1,
expanded: false,
));
expect(globalKey1.currentState.outerController.position.pixels, 0.0);
globalKey1.currentState.innerController.position.setPixels(10.0);
expect(globalKey1.currentState.innerController.position.pixels, 10.0);
final double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// Manipulating Inner
globalKey1.currentState.innerController.jumpTo(100.0);
expect(globalKey1.currentState.innerController.position.pixels, 100.0);
expect(
globalKey1.currentState.outerController.position.pixels,
appBarHeight,
);
globalKey1.currentState.innerController.jumpTo(0.0);
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
expect(
globalKey1.currentState.outerController.position.pixels,
appBarHeight,
);
// Reset
final GlobalKey<NestedScrollViewState> globalKey2 = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey2,
expanded: false,
));
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
globalKey2.currentState.innerController.position.setPixels(10.0);
expect(globalKey2.currentState.innerController.position.pixels, 10.0);
// Manipulating Outer
globalKey2.currentState.outerController.jumpTo(100.0);
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
expect(globalKey2.currentState.outerController.position.pixels, 100.0);
globalKey2.currentState.outerController.jumpTo(0.0);
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
});
testWidgets('outer: scrolled, inner: not scrolled', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey1,
expanded: false,
));
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
globalKey1.currentState.outerController.position.setPixels(10.0);
expect(globalKey1.currentState.outerController.position.pixels, 10.0);
final double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// Manipulating Inner
globalKey1.currentState.innerController.jumpTo(100.0);
expect(globalKey1.currentState.innerController.position.pixels, 100.0);
expect(
globalKey1.currentState.outerController.position.pixels,
appBarHeight,
);
globalKey1.currentState.innerController.jumpTo(0.0);
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
expect(
globalKey1.currentState.outerController.position.pixels,
appBarHeight,
);
// Reset
final GlobalKey<NestedScrollViewState> globalKey2 = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey2,
expanded: false,
));
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
globalKey2.currentState.outerController.position.setPixels(10.0);
expect(globalKey2.currentState.outerController.position.pixels, 10.0);
// Manipulating Outer
globalKey2.currentState.outerController.jumpTo(100.0);
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
expect(globalKey2.currentState.outerController.position.pixels, 100.0);
globalKey2.currentState.outerController.jumpTo(0.0);
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
});
testWidgets('outer: scrolled, inner: scrolled', (WidgetTester tester) async {
final GlobalKey<NestedScrollViewState> globalKey1 = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey1,
expanded: false,
));
globalKey1.currentState.innerController.position.setPixels(10.0);
expect(globalKey1.currentState.innerController.position.pixels, 10.0);
globalKey1.currentState.outerController.position.setPixels(10.0);
expect(globalKey1.currentState.outerController.position.pixels, 10.0);
final double appBarHeight = tester.renderObject<RenderBox>(find.byType(AppBar)).size.height;
// Manipulating Inner
globalKey1.currentState.innerController.jumpTo(100.0);
expect(globalKey1.currentState.innerController.position.pixels, 100.0);
expect(
globalKey1.currentState.outerController.position.pixels,
appBarHeight,
);
globalKey1.currentState.innerController.jumpTo(0.0);
expect(globalKey1.currentState.innerController.position.pixels, 0.0);
expect(
globalKey1.currentState.outerController.position.pixels,
appBarHeight,
);
// Reset
final GlobalKey<NestedScrollViewState> globalKey2 = GlobalKey();
await tester.pumpWidget(buildTest(
key: globalKey2,
expanded: false,
));
globalKey2.currentState.innerController.position.setPixels(10.0);
expect(globalKey2.currentState.innerController.position.pixels, 10.0);
globalKey2.currentState.outerController.position.setPixels(10.0);
expect(globalKey2.currentState.outerController.position.pixels, 10.0);
// Manipulating Outer
globalKey2.currentState.outerController.jumpTo(100.0);
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
expect(globalKey2.currentState.outerController.position.pixels, 100.0);
globalKey2.currentState.outerController.jumpTo(0.0);
expect(globalKey2.currentState.innerController.position.pixels, 0.0);
expect(globalKey2.currentState.outerController.position.pixels, 0.0);
});
});
});
// Regression test for https://github.com/flutter/flutter/issues/39963.
testWidgets('NestedScrollView with SliverOverlapAbsorber in or out of the first screen', (WidgetTester tester) async {
await tester.pumpWidget(const _TestLayoutExtentIsNegative(1));
......
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