Unverified Commit 98bc1766 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Have the physics enforce the scroll position. (#56521)

parent 324fdb6b
...@@ -214,7 +214,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda ...@@ -214,7 +214,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
) { ) {
possibleFirstIndex -= 1; possibleFirstIndex -= 1;
} }
max = possibleFirstIndex * itemExtent; max = (possibleFirstIndex + 1) * itemExtent;
} }
geometry = SliverGeometry( geometry = SliverGeometry(
scrollExtent: max, scrollExtent: max,
......
...@@ -111,12 +111,24 @@ abstract class ScrollActivity { ...@@ -111,12 +111,24 @@ abstract class ScrollActivity {
/// Whether the scroll view should ignore pointer events while performing this /// Whether the scroll view should ignore pointer events while performing this
/// activity. /// activity.
///
/// See also:
///
/// * [isScrolling], which describes whether the activity is considered
/// to represent user interaction or not.
bool get shouldIgnorePointer; bool get shouldIgnorePointer;
/// Whether performing this activity constitutes scrolling. /// Whether performing this activity constitutes scrolling.
/// ///
/// Used, for example, to determine whether the user scroll direction is /// Used, for example, to determine whether the user scroll
/// direction (see [ScrollPosition.userScrollDirection]) is
/// [ScrollDirection.idle]. /// [ScrollDirection.idle].
///
/// See also:
///
/// * [shouldIgnorePointer], which controls whether pointer events
/// are allowed while the activity is live.
/// * [UserScrollNotification], which exposes this status.
bool get isScrolling; bool get isScrolling;
/// If applicable, the velocity at which the scroll offset is currently /// If applicable, the velocity at which the scroll offset is currently
...@@ -515,9 +527,6 @@ class BallisticScrollActivity extends ScrollActivity { ...@@ -515,9 +527,6 @@ class BallisticScrollActivity extends ScrollActivity {
.whenComplete(_end); // won't trigger if we dispose _controller first .whenComplete(_end); // won't trigger if we dispose _controller first
} }
@override
double get velocity => _controller.velocity;
AnimationController _controller; AnimationController _controller;
@override @override
...@@ -562,6 +571,9 @@ class BallisticScrollActivity extends ScrollActivity { ...@@ -562,6 +571,9 @@ class BallisticScrollActivity extends ScrollActivity {
@override @override
bool get isScrolling => true; bool get isScrolling => true;
@override
double get velocity => _controller.velocity;
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
...@@ -622,9 +634,6 @@ class DrivenScrollActivity extends ScrollActivity { ...@@ -622,9 +634,6 @@ class DrivenScrollActivity extends ScrollActivity {
/// animation to stop before it reaches the end. /// animation to stop before it reaches the end.
Future<void> get done => _completer.future; Future<void> get done => _completer.future;
@override
double get velocity => _controller.velocity;
void _tick() { void _tick() {
if (delegate.setPixels(_controller.value) != 0.0) if (delegate.setPixels(_controller.value) != 0.0)
delegate.goIdle(); delegate.goIdle();
...@@ -645,6 +654,9 @@ class DrivenScrollActivity extends ScrollActivity { ...@@ -645,6 +654,9 @@ class DrivenScrollActivity extends ScrollActivity {
@override @override
bool get isScrolling => true; bool get isScrolling => true;
@override
double get velocity => _controller.velocity;
@override @override
void dispose() { void dispose() {
_completer.complete(); _completer.complete();
......
...@@ -50,20 +50,24 @@ class ScrollBehavior { ...@@ -50,20 +50,24 @@ class ScrollBehavior {
return null; return null;
} }
static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
static const ScrollPhysics _clampingPhysics = ClampingScrollPhysics(parent: RangeMaintainingScrollPhysics());
/// The scroll physics to use for the platform given by [getPlatform]. /// The scroll physics to use for the platform given by [getPlatform].
/// ///
/// Defaults to [BouncingScrollPhysics] on iOS and [ClampingScrollPhysics] on /// Defaults to [RangeMaintainingScrollPhysics] mixed with
/// [BouncingScrollPhysics] on iOS and [ClampingScrollPhysics] on
/// Android. /// Android.
ScrollPhysics getScrollPhysics(BuildContext context) { ScrollPhysics getScrollPhysics(BuildContext context) {
switch (getPlatform(context)) { switch (getPlatform(context)) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
return const BouncingScrollPhysics(); return _bouncingPhysics;
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.windows: case TargetPlatform.windows:
return const ClampingScrollPhysics(); return _clampingPhysics;
} }
return null; return null;
} }
......
...@@ -36,6 +36,20 @@ export 'package:flutter/physics.dart' show Simulation, ScrollSpringSimulation, T ...@@ -36,6 +36,20 @@ export 'package:flutter/physics.dart' show Simulation, ScrollSpringSimulation, T
/// ///
/// Instead of creating your own subclasses, [parent] can be used to combine /// Instead of creating your own subclasses, [parent] can be used to combine
/// [ScrollPhysics] objects of different types to get the desired scroll physics. /// [ScrollPhysics] objects of different types to get the desired scroll physics.
/// For example:
///
/// ```dart
/// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
/// ```
///
/// You can also use `applyTo`, which is useful when you already have
/// an instance of `ScrollPhysics`:
///
/// ```dart
/// ScrollPhysics physics = const BouncingScrollPhysics();
/// // ...
/// physics.applyTo(const AlwaysScrollableScrollPhysics())
/// ```
@immutable @immutable
class ScrollPhysics { class ScrollPhysics {
/// Creates an object with the default scroll physics. /// Creates an object with the default scroll physics.
...@@ -49,9 +63,9 @@ class ScrollPhysics { ...@@ -49,9 +63,9 @@ class ScrollPhysics {
/// [ScrollPhysics] subclasses at runtime. For example: /// [ScrollPhysics] subclasses at runtime. For example:
/// ///
/// ```dart /// ```dart
/// BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()) /// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
///
/// ``` /// ```
///
/// will result in a [ScrollPhysics] that has the combined behavior /// will result in a [ScrollPhysics] that has the combined behavior
/// of [BouncingScrollPhysics] and [AlwaysScrollableScrollPhysics]: /// of [BouncingScrollPhysics] and [AlwaysScrollableScrollPhysics]:
/// behaviors that are not specified in [BouncingScrollPhysics] /// behaviors that are not specified in [BouncingScrollPhysics]
...@@ -228,6 +242,61 @@ class ScrollPhysics { ...@@ -228,6 +242,61 @@ class ScrollPhysics {
return parent.applyBoundaryConditions(position, value); return parent.applyBoundaryConditions(position, value);
} }
/// Describes what the scroll position should be given new viewport dimensions.
///
/// This is called by [ScrollPosition.correctForNewDimensions].
///
/// The arguments consist of the scroll metrics as they stood in the previous
/// frame and the scroll metrics as they now stand after the last layout,
/// including the position and minimum and maximum scroll extents; a flag
/// indicating if the current [ScrollActivity] considers that the user is
/// actively scrolling (see [ScrollActivity.isScrolling]); and the current
/// velocity of the scroll position, if it is being driven by the scroll
/// activity (this is 0.0 during a user gesture) (see
/// [ScrollActivity.velocity]).
///
/// The scroll metrics will be identical except for the
/// [ScrollMetrics.minScrollExtent] and [ScrollMetrics.maxScrollExtent]. They
/// are referred to as the `oldPosition` and `newPosition` (even though they
/// both technically have the same "position", in the form of
/// [ScrollMetrics.pixels]) because they are generated from the
/// [ScrollPosition] before and after updating the scroll extents.
///
/// If the returned value does not exactly match the scroll offset given by
/// the `newPosition` argument (see [ScrollMetrics.pixels]), then the
/// [ScrollPosition] will call [ScrollPosition.correctPixels] to update the
/// new scroll position to the returned value, and layout will be re-run. This
/// is expensive. The new value is subject to further manipulation by
/// [applyBoundaryConditions].
///
/// If the returned value _does_ match the `newPosition.pixels` scroll offset
/// exactly, then [ScrollPosition.applyNewDimensions] will be called next. In
/// that case, [applyBoundaryConditions] is not applied to the return value.
///
/// The given [ScrollMetrics] are only valid during this method call. Do not
/// keep references to them to use later, as the values may update, may not
/// update, or may update to reflect an entirely unrelated scrollable.
///
/// The default implementation returns the [ScrollMetrics.pixels] of the
/// `newPosition`, which indicates that the current scroll offset is
/// acceptable.
///
/// See also:
///
/// * [RangeMaintainingScrollPhysics], which is enabled by default, and
/// which prevents unexpected changes to the content dimensions from
/// causing the scroll position to get any further out of bounds.
double adjustPositionForNewDimensions({
@required ScrollMetrics oldPosition,
@required ScrollMetrics newPosition,
@required bool isScrolling,
@required double velocity,
}) {
if (parent == null)
return newPosition.pixels;
return parent.adjustPositionForNewDimensions(oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity);
}
/// Returns a simulation for ballistic scrolling starting from the given /// Returns a simulation for ballistic scrolling starting from the given
/// position with the given velocity. /// position with the given velocity.
/// ///
...@@ -328,6 +397,49 @@ class ScrollPhysics { ...@@ -328,6 +397,49 @@ class ScrollPhysics {
} }
} }
/// Scroll physics that attempt to keep the scroll position in range when the
/// contents change dimensions suddenly.
///
/// If the scroll position is already out of range, this attempts to maintain
/// the amount of overscroll or underscroll already present.
///
/// If the scroll activity is animating the scroll position, sudden changes to
/// the scroll dimensions are allowed to happen (so as to prevent animations
/// from jumping back and forth between in-range and out-of-range values).
///
/// These physics should be combined with other scroll physics, e.g.
/// [BouncingScrollPhysics] or [ClampingScrollPhysics], to obtain a complete
/// description of typical scroll physics. See [applyTo].
class RangeMaintainingScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that maintain the scroll position in range.
const RangeMaintainingScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
@override
RangeMaintainingScrollPhysics applyTo(ScrollPhysics ancestor) {
return RangeMaintainingScrollPhysics(parent: buildParent(ancestor));
}
@override
double adjustPositionForNewDimensions({
@required ScrollMetrics oldPosition,
@required ScrollMetrics newPosition,
@required bool isScrolling,
@required double velocity,
}) {
if (velocity != 0.0 || ((oldPosition.minScrollExtent == newPosition.minScrollExtent) && (oldPosition.maxScrollExtent == newPosition.maxScrollExtent)))
return super.adjustPositionForNewDimensions(oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity);
if (oldPosition.pixels < oldPosition.minScrollExtent) {
final double oldDelta = oldPosition.minScrollExtent - oldPosition.pixels;
return newPosition.minScrollExtent - oldDelta;
}
if (oldPosition.pixels > oldPosition.maxScrollExtent) {
final double oldDelta = oldPosition.pixels - oldPosition.maxScrollExtent;
return newPosition.maxScrollExtent + oldDelta;
}
return newPosition.pixels.clamp(newPosition.minScrollExtent, newPosition.maxScrollExtent) as double;
}
}
/// Scroll physics for environments that allow the scroll offset to go beyond /// Scroll physics for environments that allow the scroll offset to go beyond
/// the bounds of the content, but then bounce the content back to the edge of /// the bounds of the content, but then bounce the content back to the edge of
/// those bounds. /// those bounds.
...@@ -534,9 +646,13 @@ class ClampingScrollPhysics extends ScrollPhysics { ...@@ -534,9 +646,13 @@ class ClampingScrollPhysics extends ScrollPhysics {
/// Scroll physics that always lets the user scroll. /// Scroll physics that always lets the user scroll.
/// ///
/// This overrides the default behavior which is to disable scrolling
/// when there is no content to scroll. It does not override the
/// handling of overscrolling.
///
/// On Android, overscrolls will be clamped by default and result in an /// On Android, overscrolls will be clamped by default and result in an
/// overscroll glow. On iOS, overscrolls will load a spring that will return /// overscroll glow. On iOS, overscrolls will load a spring that will return the
/// the scroll view to its normal range when released. /// scroll view to its normal range when released.
/// ///
/// See also: /// See also:
/// ///
......
...@@ -489,11 +489,44 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -489,11 +489,44 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
assert(minScrollExtent != null); assert(minScrollExtent != null);
assert(maxScrollExtent != null); assert(maxScrollExtent != null);
assert(minScrollExtent <= maxScrollExtent); assert(minScrollExtent <= maxScrollExtent);
final ScrollMetrics oldPosition = haveDimensions ? copyWith() : null;
_minScrollExtent = minScrollExtent; _minScrollExtent = minScrollExtent;
_maxScrollExtent = maxScrollExtent; _maxScrollExtent = maxScrollExtent;
final ScrollMetrics newPosition = haveDimensions ? copyWith() : null;
_didChangeViewportDimensionOrReceiveCorrection = false;
if (haveDimensions && !correctForNewDimensions(oldPosition, newPosition))
return false;
_haveDimensions = true; _haveDimensions = true;
applyNewDimensions(); applyNewDimensions();
_didChangeViewportDimensionOrReceiveCorrection = false; }
assert(!_didChangeViewportDimensionOrReceiveCorrection, 'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().');
return true;
}
/// Verifies that the new content and viewport dimensions are acceptable.
///
/// Called by [applyContentDimensions] to determine its return value.
///
/// Should return true if the current scroll offset is correct given
/// the new content and viewport dimensions.
///
/// Otherwise, should call [correctPixels] to correct the scroll
/// offset given the new dimensions, and then return false.
///
/// This is only called when [haveDimensions] is true.
///
/// The default implementation defers to [ScrollPhysics.adjustPositionForNewDimensions].
@protected
bool correctForNewDimensions(ScrollMetrics oldPosition, ScrollMetrics newPosition) {
final double newPixels = physics.adjustPositionForNewDimensions(
oldPosition: oldPosition,
newPosition: newPosition,
isScrolling: activity.isScrolling,
velocity: activity.velocity,
);
if (newPixels != pixels) {
correctPixels(newPixels);
return false;
} }
return true; return true;
} }
......
...@@ -289,8 +289,9 @@ void main() { ...@@ -289,8 +289,9 @@ void main() {
' │ crossAxisDirection: right\n' ' │ crossAxisDirection: right\n'
' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range:\n' ' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range:\n'
' │ 0.0..39400.0, viewport: 600.0, ScrollableState,\n' ' │ 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n' ' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics ->\n'
' │ IdleScrollActivity#00000, ScrollDirection.idle)\n' ' │ RangeMaintainingScrollPhysics, IdleScrollActivity#00000,\n'
' │ ScrollDirection.idle)\n'
' │ anchor: 0.0\n' ' │ anchor: 0.0\n'
' │\n' ' │\n'
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n' ' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
...@@ -436,8 +437,9 @@ void main() { ...@@ -436,8 +437,9 @@ void main() {
' │ crossAxisDirection: right\n' ' │ crossAxisDirection: right\n'
' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0,\n' ' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0,\n'
' │ range: 0.0..39400.0, viewport: 600.0, ScrollableState,\n' ' │ range: 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n' ' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics ->\n'
' │ IdleScrollActivity#00000, ScrollDirection.idle)\n' ' │ RangeMaintainingScrollPhysics, IdleScrollActivity#00000,\n'
' │ ScrollDirection.idle)\n'
' │ anchor: 0.0\n' ' │ anchor: 0.0\n'
' │\n' ' │\n'
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n' ' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
......
// Copyright 2014 The Flutter 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
class ExpandingBox extends StatefulWidget {
const ExpandingBox({this.collapsedSize, this.expandedSize});
final double collapsedSize;
final double expandedSize;
@override
State<ExpandingBox> createState() => _ExpandingBoxState();
}
class _ExpandingBoxState extends State<ExpandingBox> with AutomaticKeepAliveClientMixin<ExpandingBox>{
double _height;
@override
void initState() {
super.initState();
_height = widget.collapsedSize;
}
void toggleSize() {
setState(() {
_height = _height == widget.collapsedSize ? widget.expandedSize : widget.collapsedSize;
});
}
@override
Widget build(BuildContext context) {
super.build(context);
return Container(
height: _height,
color: Colors.green,
child: Align(
alignment: Alignment.bottomCenter,
child: FlatButton(
child: const Text('Collapse'),
onPressed: toggleSize,
),
),
);
}
@override
bool get wantKeepAlive => true;
}
void main() {
testWidgets('shrink listview', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: ListView.builder(
itemBuilder: (BuildContext context, int index) => index == 0
? const ExpandingBox(collapsedSize: 400, expandedSize: 1200)
: Container(height: 300, color: Colors.red),
itemCount: 2,
),
));
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
expect(position.activity, isInstanceOf<IdleScrollActivity>());
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 100.0);
expect(position.pixels, 0.0);
await tester.tap(find.byType(FlatButton));
await tester.pump();
final TestGesture drag1 = await tester.startGesture(const Offset(10.0, 500.0));
await tester.pump();
await drag1.moveTo(const Offset(10.0, 0.0));
await tester.pump();
await drag1.up();
await tester.pump();
expect(position.pixels, moreOrLessEquals(500.0));
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 900.0);
final TestGesture drag2 = await tester.startGesture(const Offset(10.0, 500.0));
await tester.pump();
await drag2.moveTo(const Offset(10.0, 100.0));
await tester.pump();
await drag2.up();
await tester.pump();
expect(position.maxScrollExtent, 900.0);
expect(position.pixels, moreOrLessEquals(900.0));
await tester.pump();
await tester.tap(find.byType(FlatButton));
await tester.pump();
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 100.0);
expect(position.pixels, 100.0);
});
testWidgets('shrink listview while dragging', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: ListView.builder(
itemBuilder: (BuildContext context, int index) => index == 0
? const ExpandingBox(collapsedSize: 400, expandedSize: 1200)
: Container(height: 300, color: Colors.red),
itemCount: 2,
),
));
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
expect(position.activity, isInstanceOf<IdleScrollActivity>());
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 100.0);
expect(position.pixels, 0.0);
await tester.tap(find.byType(FlatButton));
await tester.pump(); // start button animation
await tester.pump(const Duration(seconds: 1)); // finish button animation
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 1800.0);
expect(position.pixels, 0.0);
final TestGesture drag1 = await tester.startGesture(const Offset(10.0, 500.0));
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
await drag1.moveTo(const Offset(10.0, 0.0));
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
await drag1.up();
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
expect(position.pixels, moreOrLessEquals(500.0));
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 900.0);
final TestGesture drag2 = await tester.startGesture(const Offset(10.0, 500.0));
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
await drag2.moveTo(const Offset(10.0, 100.0));
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
expect(position.maxScrollExtent, 900.0);
expect(position.pixels, lessThanOrEqualTo(900.0));
expect(position.activity, isInstanceOf<DragScrollActivity>());
final _ExpandingBoxState expandingBoxState = tester.state<_ExpandingBoxState>(find.byType(ExpandingBox));
expandingBoxState.toggleSize();
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
expect(position.activity, isInstanceOf<DragScrollActivity>());
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 100.0);
expect(position.pixels, 100.0);
await drag2.moveTo(const Offset(10.0, 150.0));
await drag2.up();
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 100.0);
expect(position.pixels, 50.0);
expect(await tester.pumpAndSettle(), 1); // Nothing to animate
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 100.0);
expect(position.pixels, 50.0);
});
testWidgets('shrink listview while ballistic', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: GestureDetector(
onTap: () { assert(false); },
child: ListView.builder(
physics: const RangeMaintainingScrollPhysics(parent: BouncingScrollPhysics()),
itemBuilder: (BuildContext context, int index) => index == 0
? const ExpandingBox(collapsedSize: 400, expandedSize: 1200)
: Container(height: 300, color: Colors.red),
itemCount: 2,
),
),
));
final _ExpandingBoxState expandingBoxState = tester.state<_ExpandingBoxState>(find.byType(ExpandingBox));
expandingBoxState.toggleSize();
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
expect(position.activity, isInstanceOf<IdleScrollActivity>());
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 100.0);
expect(position.pixels, 0.0);
await tester.pump();
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 1800.0);
expect(position.pixels, 0.0);
final TestGesture drag1 = await tester.startGesture(const Offset(10.0, 10.0));
await tester.pump();
expect(position.activity, isInstanceOf<HoldScrollActivity>());
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 1800.0);
expect(position.pixels, 0.0);
await drag1.moveTo(const Offset(10.0, 50.0)); // to get past the slop and trigger the drag
await drag1.moveTo(const Offset(10.0, 550.0));
expect(position.pixels, -500.0);
await tester.pump();
expect(position.activity, isInstanceOf<DragScrollActivity>());
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 1800.0);
expect(position.pixels, -500.0);
await drag1.up();
await tester.pump();
expect(position.activity, isInstanceOf<BallisticScrollActivity>());
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 1800.0);
expect(position.pixels, -500.0);
expandingBoxState.toggleSize();
await tester.pump(); // apply physics without moving clock forward
expect(position.activity, isInstanceOf<BallisticScrollActivity>());
// TODO(ianh): Determine why the maxScrollOffset is 200.0 here instead of 100.0 or double.infinity.
// expect(position.minScrollExtent, 0.0);
// expect(position.maxScrollExtent, 100.0);
expect(position.pixels, -500.0);
await tester.pumpAndSettle(); // ignoring the exact effects of the animation
expect(position.activity, isInstanceOf<IdleScrollActivity>());
expect(position.minScrollExtent, 0.0);
expect(position.maxScrollExtent, 100.0);
expect(position.pixels, 0.0);
});
}
...@@ -136,7 +136,7 @@ void main() { ...@@ -136,7 +136,7 @@ void main() {
viewportHeight: viewportHeight, viewportHeight: viewportHeight,
)); ));
final int frames = await tester.pumpAndSettle(); final int frames = await tester.pumpAndSettle();
expect(frames, greaterThan(1)); // ensure animation to bring tile17 into view expect(frames, 1); // No animation when content shrinks suddenly.
expect(controller.offset, scrollPosition - itemHeight); expect(controller.offset, scrollPosition - itemHeight);
expect(find.text('Tile 0'), findsNothing); expect(find.text('Tile 0'), findsNothing);
......
...@@ -384,11 +384,26 @@ void main() { ...@@ -384,11 +384,26 @@ void main() {
), ),
), ),
); );
expect(find.text('Page 0'), findsNothing);
expect(find.text('Page 6'), findsNothing);
await tester.drag(find.text('Page 5'), const Offset(0, -1000)); await tester.drag(find.text('Page 5'), const Offset(0, -1000));
// Controller will be temporarily over-scrolled. // Controller will be temporarily over-scrolled (before the frame triggered by the drag) because
// SliverFixedExtentList doesn't report its size until it has built its last child, so the
// maxScrollExtent is infinite, so when we move by 1000 pixels in one go, we go all the way.
//
// This never actually gets rendered, it's just the controller state before we lay out.
expect(controller.offset, 1600.0); expect(controller.offset, 1600.0);
await tester.pumpAndSettle();
// It will be corrected after a auto scroll animation. // However, once we pump, the scroll offset gets clamped to the newly discovered maximum, which
// is the itemExtent (200) times the number of items (7) minus the height of the viewport (600).
// This adds up to 800.0.
await tester.pump();
expect(find.text('Page 0'), findsNothing);
expect(find.text('Page 6'), findsOneWidget);
expect(controller.offset, 800.0);
expect(await tester.pumpAndSettle(), 1); // there should be no animation here
expect(controller.offset, 800.0); expect(controller.offset, 800.0);
}); });
......
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