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.
This commit is contained in:
parent
85b2b86939
commit
f64bfba860
@ -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)';
|
||||
}
|
||||
|
@ -116,6 +116,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
_pendingDragOffset = Offset.zero;
|
||||
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
|
||||
} else if (_state == _DragState.accepted) {
|
||||
resolve(GestureDisposition.accepted);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -74,12 +74,12 @@ class NestedScrollView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NestedScrollViewState extends State<NestedScrollView> {
|
||||
_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
|
||||
|
@ -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)';
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -361,35 +361,49 @@ class ScrollableState extends State<Scrollable> 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;
|
||||
}
|
||||
|
@ -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<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) {
|
||||
final HorizontalDragGestureRecognizer drag = new HorizontalDragGestureRecognizer();
|
||||
|
||||
|
@ -139,6 +139,11 @@ void main() {
|
||||
final List<String> log = <String>[];
|
||||
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));
|
||||
@ -153,6 +158,25 @@ void main() {
|
||||
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>[]));
|
||||
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.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(<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 up ends', (WidgetTester tester) async {
|
||||
final List<String> log = <String>[];
|
||||
await tester.pumpWidget(_buildScroller(log: log));
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user