diff --git a/packages/flutter/lib/src/gestures/drag_details.dart b/packages/flutter/lib/src/gestures/drag_details.dart index 476de8f8de..a8f5a06f91 100644 --- a/packages/flutter/lib/src/gestures/drag_details.dart +++ b/packages/flutter/lib/src/gestures/drag_details.dart @@ -62,6 +62,10 @@ class DragStartDetails { /// Defaults to the origin if not specified in the constructor. final Offset globalPosition; + // TODO(ianh): Expose the current position, so that you can have a no-jump + // drag even when disambiguating (though of course it would lag the finger + // instead). + @override String toString() => '$runtimeType($globalPosition)'; } diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart index a5a86c9713..c00b5dda75 100644 --- a/packages/flutter/lib/src/gestures/monodrag.dart +++ b/packages/flutter/lib/src/gestures/monodrag.dart @@ -116,6 +116,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { _pendingDragOffset = Offset.zero; if (onDown != null) invokeCallback('onDown', () => onDown(new DragDownDetails(globalPosition: _initialPosition))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504 + } else if (_state == _DragState.accepted) { + resolve(GestureDisposition.accepted); } } diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index fc820bd79c..e0f522303b 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -504,6 +504,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { { final HorizontalDragGestureRecognizer recognizer = owner._recognizers[HorizontalDragGestureRecognizer]; if (recognizer != null) { + if (recognizer.onDown != null) + recognizer.onDown(new DragDownDetails()); if (recognizer.onStart != null) recognizer.onStart(new DragStartDetails()); if (recognizer.onUpdate != null) @@ -516,6 +518,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { { final PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer]; if (recognizer != null) { + if (recognizer.onDown != null) + recognizer.onDown(new DragDownDetails()); if (recognizer.onStart != null) recognizer.onStart(new DragStartDetails()); if (recognizer.onUpdate != null) @@ -531,6 +535,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { { final VerticalDragGestureRecognizer recognizer = owner._recognizers[VerticalDragGestureRecognizer]; if (recognizer != null) { + if (recognizer.onDown != null) + recognizer.onDown(new DragDownDetails()); if (recognizer.onStart != null) recognizer.onStart(new DragStartDetails()); if (recognizer.onUpdate != null) @@ -543,6 +549,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { { final PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer]; if (recognizer != null) { + if (recognizer.onDown != null) + recognizer.onDown(new DragDownDetails()); if (recognizer.onStart != null) recognizer.onStart(new DragStartDetails()); if (recognizer.onUpdate != null) diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index feeaa4fbb3..432c3c5b1b 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -74,12 +74,12 @@ class NestedScrollView extends StatefulWidget { } class _NestedScrollViewState extends State { - _NestedScrollCoorindator _coordinator; + _NestedScrollCoordinator _coordinator; @override void initState() { super.initState(); - _coordinator = new _NestedScrollCoorindator(context, widget.initialScrollOffset); + _coordinator = new _NestedScrollCoordinator(context, widget.initialScrollOffset); } @override @@ -134,8 +134,8 @@ class _NestedScrollMetrics extends FixedScrollMetrics { typedef ScrollActivity _NestedScrollActivityGetter(_NestedScrollPosition position); -class _NestedScrollCoorindator implements ScrollActivityDelegate { - _NestedScrollCoorindator(this._context, double initialScrollOffset) { +class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldController { + _NestedScrollCoordinator(this._context, double initialScrollOffset) { _outerController = new _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'outer'); _innerController = new _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'inner'); } @@ -408,10 +408,17 @@ class _NestedScrollCoorindator implements ScrollActivityDelegate { return 0.0; } - void didTouch() { - _outerPosition._propagateTouched(); - for (_NestedScrollPosition position in _innerPositions) - position._propagateTouched(); + ScrollHoldController hold(VoidCallback holdCancelCallback) { + beginActivity( + new HoldScrollActivity(delegate: _outerPosition, onHoldCanceled: holdCancelCallback), + (_NestedScrollPosition position) => new HoldScrollActivity(delegate: position), + ); + return this; + } + + @override + void cancel() { + goBallistic(0.0); } Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { @@ -484,12 +491,12 @@ class _NestedScrollCoorindator implements ScrollActivityDelegate { } class _NestedScrollController extends ScrollController { - _NestedScrollController(this.coorindator, { + _NestedScrollController(this.coordinator, { double initialScrollOffset: 0.0, String debugLabel, }) : super(initialScrollOffset: initialScrollOffset, debugLabel: debugLabel); - final _NestedScrollCoorindator coorindator; + final _NestedScrollCoordinator coordinator; @override ScrollPosition createScrollPosition( @@ -498,7 +505,7 @@ class _NestedScrollController extends ScrollController { ScrollPosition oldPosition, ) { return new _NestedScrollPosition( - coorindator: coorindator, + coordinator: coordinator, physics: physics, context: context, initialPixels: initialScrollOffset, @@ -511,8 +518,8 @@ class _NestedScrollController extends ScrollController { void attach(ScrollPosition position) { assert(position is _NestedScrollPosition); super.attach(position); - coorindator.updateParent(); - coorindator.updateCanDrag(); + coordinator.updateParent(); + coordinator.updateCanDrag(); } Iterable<_NestedScrollPosition> get nestedPositions sync* { @@ -527,7 +534,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele double initialPixels: 0.0, ScrollPosition oldPosition, String debugLabel, - @required this.coorindator, + @required this.coordinator, }) : super( physics: physics, context: context, @@ -541,7 +548,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele assert(activity != null); } - final _NestedScrollCoorindator coorindator; + final _NestedScrollCoordinator coordinator; TickerProvider get vsync => context.vsync; @@ -603,7 +610,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele } @override - ScrollDirection get userScrollDirection => coorindator.userScrollDirection; + ScrollDirection get userScrollDirection => coordinator.userScrollDirection; DrivenScrollActivity createDrivenScrollActivity(double to, Duration duration, Curve curve) { return new DrivenScrollActivity( @@ -652,9 +659,9 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele assert(metrics != null); if (metrics.minRange == metrics.maxRange) return new IdleScrollActivity(this); - return new _NestedOuterBallisticScrollActivity(coorindator, this, metrics, simulation, context.vsync); + return new _NestedOuterBallisticScrollActivity(coordinator, this, metrics, simulation, context.vsync); case _NestedBallisticScrollActivityMode.inner: - return new _NestedInnerBallisticScrollActivity(coorindator, this, simulation, context.vsync); + return new _NestedInnerBallisticScrollActivity(coordinator, this, simulation, context.vsync); case _NestedBallisticScrollActivityMode.independent: return new BallisticScrollActivity(this, simulation, context.vsync); } @@ -666,12 +673,12 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele @required Duration duration, @required Curve curve, }) { - return coorindator.animateTo(coorindator.unnestOffset(to, this), duration: duration, curve: curve); + return coordinator.animateTo(coordinator.unnestOffset(to, this), duration: duration, curve: curve); } @override void jumpTo(double value) { - return coorindator.jumpTo(coorindator.unnestOffset(value, this)); + return coordinator.jumpTo(coordinator.unnestOffset(value, this)); } @override @@ -692,7 +699,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele @override void applyNewDimensions() { super.applyNewDimensions(); - coorindator.updateCanDrag(); + coordinator.updateCanDrag(); } void updateCanDrag(double totalExtent) { @@ -700,17 +707,13 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele } @override - void didTouch() { - coorindator.didTouch(); - } - - void _propagateTouched() { - activity.didTouch(); + ScrollHoldController hold(VoidCallback holdCancelCallback) { + return coordinator.hold(holdCancelCallback); } @override Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { - return coorindator.drag(details, dragCancelCallback); + return coordinator.drag(details, dragCancelCallback); } @override @@ -724,36 +727,36 @@ enum _NestedBallisticScrollActivityMode { outer, inner, independent } class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity { _NestedInnerBallisticScrollActivity( - this.coorindator, + this.coordinator, _NestedScrollPosition position, Simulation simulation, TickerProvider vsync, ) : super(position, simulation, vsync); - final _NestedScrollCoorindator coorindator; + final _NestedScrollCoordinator coordinator; @override _NestedScrollPosition get delegate => super.delegate; @override void resetActivity() { - delegate.beginActivity(coorindator.createInnerBallisticScrollActivity(delegate, velocity)); + delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity)); } @override void applyNewDimensions() { - delegate.beginActivity(coorindator.createInnerBallisticScrollActivity(delegate, velocity)); + delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity)); } @override bool applyMoveTo(double value) { - return super.applyMoveTo(coorindator.nestOffset(value, delegate)); + return super.applyMoveTo(coordinator.nestOffset(value, delegate)); } } class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { _NestedOuterBallisticScrollActivity( - this.coorindator, + this.coordinator, _NestedScrollPosition position, this.metrics, Simulation simulation, @@ -763,7 +766,7 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { assert(metrics.maxRange > metrics.minRange); } - final _NestedScrollCoorindator coorindator; + final _NestedScrollCoordinator coordinator; final _NestedScrollMetrics metrics; @override @@ -771,12 +774,12 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { @override void resetActivity() { - delegate.beginActivity(coorindator.createOuterBallisticScrollActivity(velocity)); + delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity)); } @override void applyNewDimensions() { - delegate.beginActivity(coorindator.createOuterBallisticScrollActivity(velocity)); + delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity)); } @override diff --git a/packages/flutter/lib/src/widgets/scroll_activity.dart b/packages/flutter/lib/src/widgets/scroll_activity.dart index 271ca47639..cefbcff00d 100644 --- a/packages/flutter/lib/src/widgets/scroll_activity.dart +++ b/packages/flutter/lib/src/widgets/scroll_activity.dart @@ -104,9 +104,6 @@ abstract class ScrollActivity { new ScrollEndNotification(metrics: metrics, context: context).dispatch(context); } - /// Called when the user touches the scroll view that is performing this activity. - void didTouch() { } - /// Called when the scroll view that is performing this activity changes its metrics. void applyNewDimensions() { } @@ -127,12 +124,15 @@ abstract class ScrollActivity { } @override - String toString() => '$runtimeType'; + String toString() => '$runtimeType#$hashCode'; } /// A scroll activity that does nothing. /// /// When a scroll view is not scrolling, it is performing the idle activity. +/// +/// If the [Scrollable] changes dimensions, this activity triggers a ballistic +/// activity to restore the view. class IdleScrollActivity extends ScrollActivity { /// Creates a scroll activity that does nothing. IdleScrollActivity(ScrollActivityDelegate delegate) : super(delegate); @@ -149,6 +149,56 @@ class IdleScrollActivity extends ScrollActivity { bool get isScrolling => false; } +/// Interface for holding a [Scrollable] stationary. +/// +/// An object that implements this interface is returned by +/// [ScrollPosition.hold]. It holds the scrollable stationary until an activity +/// is started or the [cancel] method is called. +abstract class ScrollHoldController { + /// Release the [Scrollable], potentially letting it go ballistic if + /// necessary. + void cancel(); +} + +/// A scroll activity that does nothing but can be released to resume +/// normal idle behavior. +/// +/// This is used while the user is touching the [Scrollable] but before the +/// touch has become a [Drag]. +/// +/// For the purposes of [ScrollNotification]s, this activity does not constitute +/// scrolling, and does not prevent the user from interacting with the contents +/// of the [Scrollable] (unlike when a drag has begun or there is a scroll +/// animation underway). +class HoldScrollActivity extends ScrollActivity implements ScrollHoldController { + /// Creates a scroll activity that does nothing. + HoldScrollActivity({ + @required ScrollActivityDelegate delegate, + this.onHoldCanceled, + }) : super(delegate); + + /// Called when [dispose] is called. + final VoidCallback onHoldCanceled; + + @override + bool get shouldIgnorePointer => false; + + @override + bool get isScrolling => false; + + @override + void cancel() { + delegate.goBallistic(0.0); + } + + @override + void dispose() { + if (onHoldCanceled != null) + onHoldCanceled(); + super.dispose(); + } +} + /// Scrolls a scroll view as the user drags their finger across the screen. /// /// See also: @@ -217,7 +267,7 @@ class ScrollDragController implements Drag { delegate.goBallistic(0.0); } - /// Called when the delegate is no longer sending events to this object. + /// Called by the delegate when it is no longer sending events to this object. @mustCallSuper void dispose() { _lastDetails = null; @@ -229,6 +279,11 @@ class ScrollDragController implements Drag { /// [DragEndDetails] object. dynamic get lastDetails => _lastDetails; dynamic _lastDetails; + + @override + String toString() { + return '$runtimeType#$hashCode'; + } } /// The activity a scroll view performs when a the user drags their finger @@ -248,11 +303,6 @@ class DragScrollActivity extends ScrollActivity { ScrollDragController _controller; - @override - void didTouch() { - assert(false); - } - @override void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) { final dynamic lastDetails = _controller.lastDetails; @@ -296,6 +346,11 @@ class DragScrollActivity extends ScrollActivity { _controller = null; super.dispose(); } + + @override + String toString() { + return '$runtimeType#$hashCode($_controller)'; + } } /// An activity that animates a scroll view based on a physics [simulation]. @@ -340,11 +395,6 @@ class BallisticScrollActivity extends ScrollActivity { delegate.goBallistic(velocity); } - @override - void didTouch() { - delegate.goIdle(); - } - @override void applyNewDimensions() { delegate.goBallistic(velocity); @@ -390,7 +440,7 @@ class BallisticScrollActivity extends ScrollActivity { @override String toString() { - return '$runtimeType($_controller)'; + return '$runtimeType#$hashCode($_controller)'; } } @@ -446,11 +496,6 @@ class DrivenScrollActivity extends ScrollActivity { /// pixels per second). double get velocity => _controller.velocity; - @override - void didTouch() { - delegate.goIdle(); - } - void _tick() { if (delegate.setPixels(_controller.value) != 0.0) delegate.goIdle(); @@ -480,6 +525,6 @@ class DrivenScrollActivity extends ScrollActivity { @override String toString() { - return '$runtimeType($_controller)'; + return '$runtimeType#$hashCode($_controller)'; } } diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart index ac48c70fe0..bd7352cbca 100644 --- a/packages/flutter/lib/src/widgets/scroll_position.dart +++ b/packages/flutter/lib/src/widgets/scroll_position.dart @@ -18,6 +18,8 @@ import 'scroll_metrics.dart'; import 'scroll_notification.dart'; import 'scroll_physics.dart'; +export 'scroll_activity.dart' show ScrollHoldController; + // ## Subclassing ScrollPosition // // * Describe how to impelement [absorb] @@ -303,7 +305,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { @Deprecated('This will lead to bugs.') void jumpToWithoutSettling(double value); - void didTouch(); + ScrollHoldController hold(VoidCallback holdCancelCallback); Drag drag(DragStartDetails details, VoidCallback dragCancelCallback); diff --git a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart index 0ee15f94be..56883201a9 100644 --- a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart +++ b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart @@ -287,16 +287,18 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc } } - /// Inform the current activity that the user touched the area to which this - /// object relates. - @override - void didTouch() { - assert(activity != null); - activity.didTouch(); - } - ScrollDragController _currentDrag; + @override + ScrollHoldController hold(VoidCallback holdCancelCallback) { + final HoldScrollActivity activity = new HoldScrollActivity( + delegate: this, + onHoldCanceled: holdCancelCallback, + ); + beginActivity(activity); + return activity; + } + /// Start a drag activity corresponding to the given [DragStartDetails]. /// /// The `onDragCanceled` argument will be invoked if the drag is ended diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 644d379c50..9c91495ebb 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -361,35 +361,49 @@ class ScrollableState extends State with TickerProviderStateMixin // TOUCH HANDLERS Drag _drag; + ScrollHoldController _hold; void _handleDragDown(DragDownDetails details) { assert(_drag == null); - position.didTouch(); + assert(_hold == null); + _hold = position.hold(_disposeHold); } void _handleDragStart(DragStartDetails details) { assert(_drag == null); + assert(_hold != null); _drag = position.drag(details, _disposeDrag); assert(_drag != null); + assert(_hold == null); } void _handleDragUpdate(DragUpdateDetails details) { // _drag might be null if the drag activity ended and called _disposeDrag. + assert(_hold == null || _drag == null); _drag?.update(details); } void _handleDragEnd(DragEndDetails details) { // _drag might be null if the drag activity ended and called _disposeDrag. + assert(_hold == null || _drag == null); _drag?.end(details); assert(_drag == null); } void _handleDragCancel() { + // _hold might be null if the drag started. // _drag might be null if the drag activity ended and called _disposeDrag. + assert(_hold == null || _drag == null); + _hold?.cancel(); _drag?.cancel(); + assert(_hold == null); assert(_drag == null); } + void _disposeHold() { + _hold = null; + } + void _disposeDrag() { _drag = null; } diff --git a/packages/flutter/test/gestures/drag_test.dart b/packages/flutter/test/gestures/drag_test.dart index cd6fd8f7ff..1f258705ee 100644 --- a/packages/flutter/test/gestures/drag_test.dart +++ b/packages/flutter/test/gestures/drag_test.dart @@ -129,6 +129,75 @@ void main() { drag.dispose(); }); + testGesture('Drag with multiple pointers', (GestureTester tester) { + final HorizontalDragGestureRecognizer drag1 = new HorizontalDragGestureRecognizer(); + final VerticalDragGestureRecognizer drag2 = new VerticalDragGestureRecognizer(); + + final List log = []; + drag1.onDown = (_) { log.add('drag1-down'); }; + drag1.onStart = (_) { log.add('drag1-start'); }; + drag1.onUpdate = (_) { log.add('drag1-update'); }; + drag1.onEnd = (_) { log.add('drag1-end'); }; + drag1.onCancel = () { log.add('drag1-cancel'); }; + drag2.onDown = (_) { log.add('drag2-down'); }; + drag2.onStart = (_) { log.add('drag2-start'); }; + drag2.onUpdate = (_) { log.add('drag2-update'); }; + drag2.onEnd = (_) { log.add('drag2-end'); }; + drag2.onCancel = () { log.add('drag2-cancel'); }; + + final TestPointer pointer5 = new TestPointer(5); + final PointerDownEvent down5 = pointer5.down(const Offset(10.0, 10.0)); + drag1.addPointer(down5); + drag2.addPointer(down5); + tester.closeArena(5); + tester.route(down5); + log.add('-a'); + + tester.route(pointer5.move(const Offset(100.0, 0.0))); + log.add('-b'); + tester.route(pointer5.move(const Offset(50.0, 50.0))); + log.add('-c'); + + final TestPointer pointer6 = new TestPointer(6); + final PointerDownEvent down6 = pointer6.down(const Offset(20.0, 20.0)); + drag1.addPointer(down6); + drag2.addPointer(down6); + tester.closeArena(6); + tester.route(down6); + log.add('-d'); + + tester.route(pointer5.move(const Offset(0.0, 100.0))); + log.add('-e'); + tester.route(pointer5.move(const Offset(70.0, 70.0))); + log.add('-f'); + + tester.route(pointer5.up()); + tester.route(pointer6.up()); + + drag1.dispose(); + drag2.dispose(); + + expect(log, [ + 'drag1-down', + 'drag2-down', + '-a', + 'drag2-cancel', + 'drag1-start', + 'drag1-update', + '-b', + 'drag1-update', + '-c', + 'drag2-down', + 'drag2-cancel', + '-d', + 'drag1-update', + '-e', + 'drag1-update', + '-f', + 'drag1-end' + ]); + }); + testGesture('Clamp max velocity', (GestureTester tester) { final HorizontalDragGestureRecognizer drag = new HorizontalDragGestureRecognizer(); diff --git a/packages/flutter/test/widgets/scroll_events_test.dart b/packages/flutter/test/widgets/scroll_events_test.dart index fc21cb85bf..5b47666799 100644 --- a/packages/flutter/test/widgets/scroll_events_test.dart +++ b/packages/flutter/test/widgets/scroll_events_test.dart @@ -139,6 +139,11 @@ void main() { final List log = []; await tester.pumpWidget(_buildScroller(log: log)); + // The ideal behaviour here would be a single start/end pair, but for + // simplicity of implementation we compromise here and accept two. Should + // you find a way to make this work with just one without complicating the + // API, feel free to change the expectation here. + expect(log, equals([])); await tester.flingFrom(const Offset(100.0, 100.0), const Offset(-50.0, -50.0), 500.0); await tester.pump(const Duration(seconds: 1)); @@ -153,6 +158,25 @@ void main() { expect(log, equals(['scroll-start', 'scroll-end', 'scroll-start', 'scroll-end'])); }); + testWidgets('fling, pause, fling generates two start/end pairs', (WidgetTester tester) async { + final List log = []; + await tester.pumpWidget(_buildScroller(log: log)); + + expect(log, equals([])); + await tester.flingFrom(const Offset(100.0, 100.0), const Offset(-50.0, -50.0), 500.0); + await tester.pump(const Duration(seconds: 1)); + log.removeWhere((String value) => value == 'scroll-update'); + expect(log, equals(['scroll-start'])); + await tester.pump(const Duration(minutes: 1)); + await tester.flingFrom(const Offset(100.0, 100.0), const Offset(-50.0, -50.0), 500.0); + log.removeWhere((String value) => value == 'scroll-update'); + expect(log, equals(['scroll-start', 'scroll-end', 'scroll-start'])); + await tester.pump(const Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); + log.removeWhere((String value) => value == 'scroll-update'); + expect(log, equals(['scroll-start', 'scroll-end', 'scroll-start', 'scroll-end'])); + }); + testWidgets('fling up ends', (WidgetTester tester) async { final List log = []; await tester.pumpWidget(_buildScroller(log: log)); diff --git a/packages/flutter/test/widgets/scrollable_test.dart b/packages/flutter/test/widgets/scrollable_test.dart index b37f3f34e2..087d51bd35 100644 --- a/packages/flutter/test/widgets/scrollable_test.dart +++ b/packages/flutter/test/widgets/scrollable_test.dart @@ -78,4 +78,21 @@ void main() { expect(result1, greaterThan(result2)); // iOS (result1) is slipperier than Android (result2) }); + + testWidgets('Holding scroll', (WidgetTester tester) async { + await pumpTest(tester, TargetPlatform.iOS); + await tester.drag(find.byType(Viewport), const Offset(0.0, 200.0)); + expect(getScrollOffset(tester), -200.0); + await tester.pump(); // trigger ballistic + await tester.pump(const Duration(milliseconds: 10)); + expect(getScrollOffset(tester), greaterThan(-200.0)); + expect(getScrollOffset(tester), lessThan(0.0)); + final double position = getScrollOffset(tester); + final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Viewport))); + expect(await tester.pumpAndSettle(), 1); + expect(getScrollOffset(tester), position); + await gesture.up(); + expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2); + expect(getScrollOffset(tester), 0.0); + }); }