Commit f64bfba8 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Tapping a ScrollView during overscroll got it stuck. (#9721)

Fixes https://github.com/flutter/flutter/issues/8476

More detailed list of changes in this patch:

* Replaced the didTouch special logic with more generic logic that
  uses Activities instead. Now instead when you tap down the
  Scrollable calls `hold()` which begins a HoldScrollActivity which is
  a hybrid of DragStartDetails and IdleScrollActivity and can be
  canceled. When you let go, it gets canceled and that goes ballistic.

* Make DragGestureRecognizer more aggressive about grabbing pointers,
  otherwise a second pointer in a situation with competing horizontal
  and vertical recognizers always gets taken by the other one.

* Fixed the _GestureSemantics widget to call the "down" callbacks so
  that it follows the same pattern as "real" interactions.

* Added tests for the above.

* Added a hashCode to ScrollActivity.toString (and subclasses).

* Added a toString to ScrollDragController, and include it in
  DragScrollActivity's toString.

* s/coorindator/coordinator/

* Add a comment in DragStartDetails to distinguish it from the
  otherwise identical DragDownDetails, so we're not tempted to merge
  them.
parent 85b2b869
...@@ -62,6 +62,10 @@ class DragStartDetails { ...@@ -62,6 +62,10 @@ class DragStartDetails {
/// Defaults to the origin if not specified in the constructor. /// Defaults to the origin if not specified in the constructor.
final Offset globalPosition; 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 @override
String toString() => '$runtimeType($globalPosition)'; String toString() => '$runtimeType($globalPosition)';
} }
......
...@@ -116,6 +116,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -116,6 +116,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
_pendingDragOffset = Offset.zero; _pendingDragOffset = Offset.zero;
if (onDown != null) if (onDown != null)
invokeCallback<Null>('onDown', () => onDown(new DragDownDetails(globalPosition: _initialPosition))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504 invokeCallback<Null>('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);
} }
} }
......
...@@ -504,6 +504,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { ...@@ -504,6 +504,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget {
{ {
final HorizontalDragGestureRecognizer recognizer = owner._recognizers[HorizontalDragGestureRecognizer]; final HorizontalDragGestureRecognizer recognizer = owner._recognizers[HorizontalDragGestureRecognizer];
if (recognizer != null) { if (recognizer != null) {
if (recognizer.onDown != null)
recognizer.onDown(new DragDownDetails());
if (recognizer.onStart != null) if (recognizer.onStart != null)
recognizer.onStart(new DragStartDetails()); recognizer.onStart(new DragStartDetails());
if (recognizer.onUpdate != null) if (recognizer.onUpdate != null)
...@@ -516,6 +518,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { ...@@ -516,6 +518,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget {
{ {
final PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer]; final PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer];
if (recognizer != null) { if (recognizer != null) {
if (recognizer.onDown != null)
recognizer.onDown(new DragDownDetails());
if (recognizer.onStart != null) if (recognizer.onStart != null)
recognizer.onStart(new DragStartDetails()); recognizer.onStart(new DragStartDetails());
if (recognizer.onUpdate != null) if (recognizer.onUpdate != null)
...@@ -531,6 +535,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { ...@@ -531,6 +535,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget {
{ {
final VerticalDragGestureRecognizer recognizer = owner._recognizers[VerticalDragGestureRecognizer]; final VerticalDragGestureRecognizer recognizer = owner._recognizers[VerticalDragGestureRecognizer];
if (recognizer != null) { if (recognizer != null) {
if (recognizer.onDown != null)
recognizer.onDown(new DragDownDetails());
if (recognizer.onStart != null) if (recognizer.onStart != null)
recognizer.onStart(new DragStartDetails()); recognizer.onStart(new DragStartDetails());
if (recognizer.onUpdate != null) if (recognizer.onUpdate != null)
...@@ -543,6 +549,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { ...@@ -543,6 +549,8 @@ class _GestureSemantics extends SingleChildRenderObjectWidget {
{ {
final PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer]; final PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer];
if (recognizer != null) { if (recognizer != null) {
if (recognizer.onDown != null)
recognizer.onDown(new DragDownDetails());
if (recognizer.onStart != null) if (recognizer.onStart != null)
recognizer.onStart(new DragStartDetails()); recognizer.onStart(new DragStartDetails());
if (recognizer.onUpdate != null) if (recognizer.onUpdate != null)
......
...@@ -74,12 +74,12 @@ class NestedScrollView extends StatefulWidget { ...@@ -74,12 +74,12 @@ class NestedScrollView extends StatefulWidget {
} }
class _NestedScrollViewState extends State<NestedScrollView> { class _NestedScrollViewState extends State<NestedScrollView> {
_NestedScrollCoorindator _coordinator; _NestedScrollCoordinator _coordinator;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_coordinator = new _NestedScrollCoorindator(context, widget.initialScrollOffset); _coordinator = new _NestedScrollCoordinator(context, widget.initialScrollOffset);
} }
@override @override
...@@ -134,8 +134,8 @@ class _NestedScrollMetrics extends FixedScrollMetrics { ...@@ -134,8 +134,8 @@ class _NestedScrollMetrics extends FixedScrollMetrics {
typedef ScrollActivity _NestedScrollActivityGetter(_NestedScrollPosition position); typedef ScrollActivity _NestedScrollActivityGetter(_NestedScrollPosition position);
class _NestedScrollCoorindator implements ScrollActivityDelegate { class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldController {
_NestedScrollCoorindator(this._context, double initialScrollOffset) { _NestedScrollCoordinator(this._context, double initialScrollOffset) {
_outerController = new _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'outer'); _outerController = new _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'outer');
_innerController = new _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'inner'); _innerController = new _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'inner');
} }
...@@ -408,10 +408,17 @@ class _NestedScrollCoorindator implements ScrollActivityDelegate { ...@@ -408,10 +408,17 @@ class _NestedScrollCoorindator implements ScrollActivityDelegate {
return 0.0; return 0.0;
} }
void didTouch() { ScrollHoldController hold(VoidCallback holdCancelCallback) {
_outerPosition._propagateTouched(); beginActivity(
for (_NestedScrollPosition position in _innerPositions) new HoldScrollActivity(delegate: _outerPosition, onHoldCanceled: holdCancelCallback),
position._propagateTouched(); (_NestedScrollPosition position) => new HoldScrollActivity(delegate: position),
);
return this;
}
@override
void cancel() {
goBallistic(0.0);
} }
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
...@@ -484,12 +491,12 @@ class _NestedScrollCoorindator implements ScrollActivityDelegate { ...@@ -484,12 +491,12 @@ class _NestedScrollCoorindator implements ScrollActivityDelegate {
} }
class _NestedScrollController extends ScrollController { class _NestedScrollController extends ScrollController {
_NestedScrollController(this.coorindator, { _NestedScrollController(this.coordinator, {
double initialScrollOffset: 0.0, double initialScrollOffset: 0.0,
String debugLabel, String debugLabel,
}) : super(initialScrollOffset: initialScrollOffset, debugLabel: debugLabel); }) : super(initialScrollOffset: initialScrollOffset, debugLabel: debugLabel);
final _NestedScrollCoorindator coorindator; final _NestedScrollCoordinator coordinator;
@override @override
ScrollPosition createScrollPosition( ScrollPosition createScrollPosition(
...@@ -498,7 +505,7 @@ class _NestedScrollController extends ScrollController { ...@@ -498,7 +505,7 @@ class _NestedScrollController extends ScrollController {
ScrollPosition oldPosition, ScrollPosition oldPosition,
) { ) {
return new _NestedScrollPosition( return new _NestedScrollPosition(
coorindator: coorindator, coordinator: coordinator,
physics: physics, physics: physics,
context: context, context: context,
initialPixels: initialScrollOffset, initialPixels: initialScrollOffset,
...@@ -511,8 +518,8 @@ class _NestedScrollController extends ScrollController { ...@@ -511,8 +518,8 @@ class _NestedScrollController extends ScrollController {
void attach(ScrollPosition position) { void attach(ScrollPosition position) {
assert(position is _NestedScrollPosition); assert(position is _NestedScrollPosition);
super.attach(position); super.attach(position);
coorindator.updateParent(); coordinator.updateParent();
coorindator.updateCanDrag(); coordinator.updateCanDrag();
} }
Iterable<_NestedScrollPosition> get nestedPositions sync* { Iterable<_NestedScrollPosition> get nestedPositions sync* {
...@@ -527,7 +534,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele ...@@ -527,7 +534,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
double initialPixels: 0.0, double initialPixels: 0.0,
ScrollPosition oldPosition, ScrollPosition oldPosition,
String debugLabel, String debugLabel,
@required this.coorindator, @required this.coordinator,
}) : super( }) : super(
physics: physics, physics: physics,
context: context, context: context,
...@@ -541,7 +548,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele ...@@ -541,7 +548,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
assert(activity != null); assert(activity != null);
} }
final _NestedScrollCoorindator coorindator; final _NestedScrollCoordinator coordinator;
TickerProvider get vsync => context.vsync; TickerProvider get vsync => context.vsync;
...@@ -603,7 +610,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele ...@@ -603,7 +610,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
} }
@override @override
ScrollDirection get userScrollDirection => coorindator.userScrollDirection; ScrollDirection get userScrollDirection => coordinator.userScrollDirection;
DrivenScrollActivity createDrivenScrollActivity(double to, Duration duration, Curve curve) { DrivenScrollActivity createDrivenScrollActivity(double to, Duration duration, Curve curve) {
return new DrivenScrollActivity( return new DrivenScrollActivity(
...@@ -652,9 +659,9 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele ...@@ -652,9 +659,9 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
assert(metrics != null); assert(metrics != null);
if (metrics.minRange == metrics.maxRange) if (metrics.minRange == metrics.maxRange)
return new IdleScrollActivity(this); 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: case _NestedBallisticScrollActivityMode.inner:
return new _NestedInnerBallisticScrollActivity(coorindator, this, simulation, context.vsync); return new _NestedInnerBallisticScrollActivity(coordinator, this, simulation, context.vsync);
case _NestedBallisticScrollActivityMode.independent: case _NestedBallisticScrollActivityMode.independent:
return new BallisticScrollActivity(this, simulation, context.vsync); return new BallisticScrollActivity(this, simulation, context.vsync);
} }
...@@ -666,12 +673,12 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele ...@@ -666,12 +673,12 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
@required Duration duration, @required Duration duration,
@required Curve curve, @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 @override
void jumpTo(double value) { void jumpTo(double value) {
return coorindator.jumpTo(coorindator.unnestOffset(value, this)); return coordinator.jumpTo(coordinator.unnestOffset(value, this));
} }
@override @override
...@@ -692,7 +699,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele ...@@ -692,7 +699,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
@override @override
void applyNewDimensions() { void applyNewDimensions() {
super.applyNewDimensions(); super.applyNewDimensions();
coorindator.updateCanDrag(); coordinator.updateCanDrag();
} }
void updateCanDrag(double totalExtent) { void updateCanDrag(double totalExtent) {
...@@ -700,17 +707,13 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele ...@@ -700,17 +707,13 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
} }
@override @override
void didTouch() { ScrollHoldController hold(VoidCallback holdCancelCallback) {
coorindator.didTouch(); return coordinator.hold(holdCancelCallback);
}
void _propagateTouched() {
activity.didTouch();
} }
@override @override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
return coorindator.drag(details, dragCancelCallback); return coordinator.drag(details, dragCancelCallback);
} }
@override @override
...@@ -724,36 +727,36 @@ enum _NestedBallisticScrollActivityMode { outer, inner, independent } ...@@ -724,36 +727,36 @@ enum _NestedBallisticScrollActivityMode { outer, inner, independent }
class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity { class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity {
_NestedInnerBallisticScrollActivity( _NestedInnerBallisticScrollActivity(
this.coorindator, this.coordinator,
_NestedScrollPosition position, _NestedScrollPosition position,
Simulation simulation, Simulation simulation,
TickerProvider vsync, TickerProvider vsync,
) : super(position, simulation, vsync); ) : super(position, simulation, vsync);
final _NestedScrollCoorindator coorindator; final _NestedScrollCoordinator coordinator;
@override @override
_NestedScrollPosition get delegate => super.delegate; _NestedScrollPosition get delegate => super.delegate;
@override @override
void resetActivity() { void resetActivity() {
delegate.beginActivity(coorindator.createInnerBallisticScrollActivity(delegate, velocity)); delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
} }
@override @override
void applyNewDimensions() { void applyNewDimensions() {
delegate.beginActivity(coorindator.createInnerBallisticScrollActivity(delegate, velocity)); delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(delegate, velocity));
} }
@override @override
bool applyMoveTo(double value) { bool applyMoveTo(double value) {
return super.applyMoveTo(coorindator.nestOffset(value, delegate)); return super.applyMoveTo(coordinator.nestOffset(value, delegate));
} }
} }
class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
_NestedOuterBallisticScrollActivity( _NestedOuterBallisticScrollActivity(
this.coorindator, this.coordinator,
_NestedScrollPosition position, _NestedScrollPosition position,
this.metrics, this.metrics,
Simulation simulation, Simulation simulation,
...@@ -763,7 +766,7 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { ...@@ -763,7 +766,7 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
assert(metrics.maxRange > metrics.minRange); assert(metrics.maxRange > metrics.minRange);
} }
final _NestedScrollCoorindator coorindator; final _NestedScrollCoordinator coordinator;
final _NestedScrollMetrics metrics; final _NestedScrollMetrics metrics;
@override @override
...@@ -771,12 +774,12 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { ...@@ -771,12 +774,12 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
@override @override
void resetActivity() { void resetActivity() {
delegate.beginActivity(coorindator.createOuterBallisticScrollActivity(velocity)); delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
} }
@override @override
void applyNewDimensions() { void applyNewDimensions() {
delegate.beginActivity(coorindator.createOuterBallisticScrollActivity(velocity)); delegate.beginActivity(coordinator.createOuterBallisticScrollActivity(velocity));
} }
@override @override
......
...@@ -104,9 +104,6 @@ abstract class ScrollActivity { ...@@ -104,9 +104,6 @@ abstract class ScrollActivity {
new ScrollEndNotification(metrics: metrics, context: context).dispatch(context); 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. /// Called when the scroll view that is performing this activity changes its metrics.
void applyNewDimensions() { } void applyNewDimensions() { }
...@@ -127,12 +124,15 @@ abstract class ScrollActivity { ...@@ -127,12 +124,15 @@ abstract class ScrollActivity {
} }
@override @override
String toString() => '$runtimeType'; String toString() => '$runtimeType#$hashCode';
} }
/// A scroll activity that does nothing. /// A scroll activity that does nothing.
/// ///
/// When a scroll view is not scrolling, it is performing the idle activity. /// 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 { class IdleScrollActivity extends ScrollActivity {
/// Creates a scroll activity that does nothing. /// Creates a scroll activity that does nothing.
IdleScrollActivity(ScrollActivityDelegate delegate) : super(delegate); IdleScrollActivity(ScrollActivityDelegate delegate) : super(delegate);
...@@ -149,6 +149,56 @@ class IdleScrollActivity extends ScrollActivity { ...@@ -149,6 +149,56 @@ class IdleScrollActivity extends ScrollActivity {
bool get isScrolling => false; 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. /// Scrolls a scroll view as the user drags their finger across the screen.
/// ///
/// See also: /// See also:
...@@ -217,7 +267,7 @@ class ScrollDragController implements Drag { ...@@ -217,7 +267,7 @@ class ScrollDragController implements Drag {
delegate.goBallistic(0.0); 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 @mustCallSuper
void dispose() { void dispose() {
_lastDetails = null; _lastDetails = null;
...@@ -229,6 +279,11 @@ class ScrollDragController implements Drag { ...@@ -229,6 +279,11 @@ class ScrollDragController implements Drag {
/// [DragEndDetails] object. /// [DragEndDetails] object.
dynamic get lastDetails => _lastDetails; dynamic get lastDetails => _lastDetails;
dynamic _lastDetails; dynamic _lastDetails;
@override
String toString() {
return '$runtimeType#$hashCode';
}
} }
/// The activity a scroll view performs when a the user drags their finger /// The activity a scroll view performs when a the user drags their finger
...@@ -248,11 +303,6 @@ class DragScrollActivity extends ScrollActivity { ...@@ -248,11 +303,6 @@ class DragScrollActivity extends ScrollActivity {
ScrollDragController _controller; ScrollDragController _controller;
@override
void didTouch() {
assert(false);
}
@override @override
void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) { void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) {
final dynamic lastDetails = _controller.lastDetails; final dynamic lastDetails = _controller.lastDetails;
...@@ -296,6 +346,11 @@ class DragScrollActivity extends ScrollActivity { ...@@ -296,6 +346,11 @@ class DragScrollActivity extends ScrollActivity {
_controller = null; _controller = null;
super.dispose(); super.dispose();
} }
@override
String toString() {
return '$runtimeType#$hashCode($_controller)';
}
} }
/// An activity that animates a scroll view based on a physics [simulation]. /// An activity that animates a scroll view based on a physics [simulation].
...@@ -340,11 +395,6 @@ class BallisticScrollActivity extends ScrollActivity { ...@@ -340,11 +395,6 @@ class BallisticScrollActivity extends ScrollActivity {
delegate.goBallistic(velocity); delegate.goBallistic(velocity);
} }
@override
void didTouch() {
delegate.goIdle();
}
@override @override
void applyNewDimensions() { void applyNewDimensions() {
delegate.goBallistic(velocity); delegate.goBallistic(velocity);
...@@ -390,7 +440,7 @@ class BallisticScrollActivity extends ScrollActivity { ...@@ -390,7 +440,7 @@ class BallisticScrollActivity extends ScrollActivity {
@override @override
String toString() { String toString() {
return '$runtimeType($_controller)'; return '$runtimeType#$hashCode($_controller)';
} }
} }
...@@ -446,11 +496,6 @@ class DrivenScrollActivity extends ScrollActivity { ...@@ -446,11 +496,6 @@ class DrivenScrollActivity extends ScrollActivity {
/// pixels per second). /// pixels per second).
double get velocity => _controller.velocity; double get velocity => _controller.velocity;
@override
void didTouch() {
delegate.goIdle();
}
void _tick() { void _tick() {
if (delegate.setPixels(_controller.value) != 0.0) if (delegate.setPixels(_controller.value) != 0.0)
delegate.goIdle(); delegate.goIdle();
...@@ -480,6 +525,6 @@ class DrivenScrollActivity extends ScrollActivity { ...@@ -480,6 +525,6 @@ class DrivenScrollActivity extends ScrollActivity {
@override @override
String toString() { String toString() {
return '$runtimeType($_controller)'; return '$runtimeType#$hashCode($_controller)';
} }
} }
...@@ -18,6 +18,8 @@ import 'scroll_metrics.dart'; ...@@ -18,6 +18,8 @@ import 'scroll_metrics.dart';
import 'scroll_notification.dart'; import 'scroll_notification.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
export 'scroll_activity.dart' show ScrollHoldController;
// ## Subclassing ScrollPosition // ## Subclassing ScrollPosition
// //
// * Describe how to impelement [absorb] // * Describe how to impelement [absorb]
...@@ -303,7 +305,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -303,7 +305,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
@Deprecated('This will lead to bugs.') @Deprecated('This will lead to bugs.')
void jumpToWithoutSettling(double value); void jumpToWithoutSettling(double value);
void didTouch(); ScrollHoldController hold(VoidCallback holdCancelCallback);
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback); Drag drag(DragStartDetails details, VoidCallback dragCancelCallback);
......
...@@ -287,16 +287,18 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc ...@@ -287,16 +287,18 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc
} }
} }
/// Inform the current activity that the user touched the area to which this ScrollDragController _currentDrag;
/// object relates.
@override @override
void didTouch() { ScrollHoldController hold(VoidCallback holdCancelCallback) {
assert(activity != null); final HoldScrollActivity activity = new HoldScrollActivity(
activity.didTouch(); delegate: this,
onHoldCanceled: holdCancelCallback,
);
beginActivity(activity);
return activity;
} }
ScrollDragController _currentDrag;
/// Start a drag activity corresponding to the given [DragStartDetails]. /// Start a drag activity corresponding to the given [DragStartDetails].
/// ///
/// The `onDragCanceled` argument will be invoked if the drag is ended /// The `onDragCanceled` argument will be invoked if the drag is ended
......
...@@ -361,35 +361,49 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -361,35 +361,49 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
// TOUCH HANDLERS // TOUCH HANDLERS
Drag _drag; Drag _drag;
ScrollHoldController _hold;
void _handleDragDown(DragDownDetails details) { void _handleDragDown(DragDownDetails details) {
assert(_drag == null); assert(_drag == null);
position.didTouch(); assert(_hold == null);
_hold = position.hold(_disposeHold);
} }
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
assert(_drag == null); assert(_drag == null);
assert(_hold != null);
_drag = position.drag(details, _disposeDrag); _drag = position.drag(details, _disposeDrag);
assert(_drag != null); assert(_drag != null);
assert(_hold == null);
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag. // _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.update(details); _drag?.update(details);
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag. // _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.end(details); _drag?.end(details);
assert(_drag == null); assert(_drag == null);
} }
void _handleDragCancel() { void _handleDragCancel() {
// _hold might be null if the drag started.
// _drag might be null if the drag activity ended and called _disposeDrag. // _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_hold?.cancel();
_drag?.cancel(); _drag?.cancel();
assert(_hold == null);
assert(_drag == null); assert(_drag == null);
} }
void _disposeHold() {
_hold = null;
}
void _disposeDrag() { void _disposeDrag() {
_drag = null; _drag = null;
} }
......
...@@ -129,6 +129,75 @@ void main() { ...@@ -129,6 +129,75 @@ void main() {
drag.dispose(); drag.dispose();
}); });
testGesture('Drag with multiple pointers', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag1 = new HorizontalDragGestureRecognizer();
final VerticalDragGestureRecognizer drag2 = new VerticalDragGestureRecognizer();
final List<String> log = <String>[];
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, <String>[
'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) { testGesture('Clamp max velocity', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = new HorizontalDragGestureRecognizer(); final HorizontalDragGestureRecognizer drag = new HorizontalDragGestureRecognizer();
......
...@@ -139,11 +139,35 @@ void main() { ...@@ -139,11 +139,35 @@ void main() {
final List<String> log = <String>[]; final List<String> log = <String>[];
await tester.pumpWidget(_buildScroller(log: 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(<String>[]));
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(<String>['scroll-start']));
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(<String>['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(<String>['scroll-start', 'scroll-end', 'scroll-start', 'scroll-end']));
});
testWidgets('fling, pause, fling generates two start/end pairs', (WidgetTester tester) async {
final List<String> log = <String>[];
await tester.pumpWidget(_buildScroller(log: log));
expect(log, equals(<String>[])); expect(log, equals(<String>[]));
await tester.flingFrom(const Offset(100.0, 100.0), const Offset(-50.0, -50.0), 500.0); await tester.flingFrom(const Offset(100.0, 100.0), const Offset(-50.0, -50.0), 500.0);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
log.removeWhere((String value) => value == 'scroll-update'); log.removeWhere((String value) => value == 'scroll-update');
expect(log, equals(<String>['scroll-start'])); expect(log, equals(<String>['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); await tester.flingFrom(const Offset(100.0, 100.0), const Offset(-50.0, -50.0), 500.0);
log.removeWhere((String value) => value == 'scroll-update'); log.removeWhere((String value) => value == 'scroll-update');
expect(log, equals(<String>['scroll-start', 'scroll-end', 'scroll-start'])); expect(log, equals(<String>['scroll-start', 'scroll-end', 'scroll-start']));
......
...@@ -78,4 +78,21 @@ void main() { ...@@ -78,4 +78,21 @@ void main() {
expect(result1, greaterThan(result2)); // iOS (result1) is slipperier than Android (result2) 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);
});
} }
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