From b40864c9b71827e4b884448a6623343be8cdeaee Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 28 Aug 2015 16:04:46 -0700 Subject: [PATCH] Disambiguate horizontal and vertical scrolling We now have separate gestures for horizontal, vertical, and pan scrolling. --- packages/flutter/lib/gestures/scroll.dart | 117 ++++++++++++------ .../flutter/lib/widgets/gesture_detector.dart | 115 +++++++++++++---- packages/flutter/lib/widgets/scrollable.dart | 10 +- packages/unit/test/gestures/scroll_test.dart | 8 +- .../unit/test/widget/pageable_list_test.dart | 2 +- 5 files changed, 181 insertions(+), 71 deletions(-) diff --git a/packages/flutter/lib/gestures/scroll.dart b/packages/flutter/lib/gestures/scroll.dart index 7be12201ad7..008af9830f2 100644 --- a/packages/flutter/lib/gestures/scroll.dart +++ b/packages/flutter/lib/gestures/scroll.dart @@ -15,43 +15,48 @@ enum ScrollState { } typedef void GestureScrollStartCallback(); -typedef void GestureScrollUpdateCallback(sky.Offset scrollDelta); +typedef void GestureScrollUpdateCallback(double scrollDelta); typedef void GestureScrollEndCallback(); -sky.Offset _getScrollOffset(sky.PointerEvent event) { - // Notice that we negate dy because scroll offsets go in the opposite direction. - return new sky.Offset(event.dx, -event.dy); -} +typedef void GesturePanStartCallback(); +typedef void GesturePanUpdateCallback(sky.Offset scrollDelta); +typedef void GesturePanEndCallback(); -class ScrollGestureRecognizer extends GestureRecognizer { - ScrollGestureRecognizer({ PointerRouter router, this.onScrollStart, this.onScrollUpdate, this.onScrollEnd }) +typedef void _GesturePolymorphicUpdateCallback(T scrollDelta); + +abstract class _ScrollGestureRecognizer extends GestureRecognizer { + _ScrollGestureRecognizer({ PointerRouter router, this.onStart, this.onUpdate, this.onEnd }) : super(router: router); - GestureScrollStartCallback onScrollStart; - GestureScrollUpdateCallback onScrollUpdate; - GestureScrollEndCallback onScrollEnd; + GestureScrollStartCallback onStart; + _GesturePolymorphicUpdateCallback onUpdate; + GestureScrollEndCallback onEnd; - ScrollState state = ScrollState.ready; - sky.Offset pendingScrollOffset; + ScrollState _state = ScrollState.ready; + T _pendingScrollDelta; + + T get _initialPendingScrollDelta; + T _getScrollDelta(sky.PointerEvent event); + bool get _hasSufficientPendingScrollDeltaToAccept; void addPointer(sky.PointerEvent event) { startTrackingPointer(event.pointer); - if (state == ScrollState.ready) { - state = ScrollState.possible; - pendingScrollOffset = sky.Offset.zero; + if (_state == ScrollState.ready) { + _state = ScrollState.possible; + _pendingScrollDelta = _initialPendingScrollDelta; } } void handleEvent(sky.PointerEvent event) { - assert(state != ScrollState.ready); + assert(_state != ScrollState.ready); if (event.type == 'pointermove') { - sky.Offset offset = _getScrollOffset(event); - if (state == ScrollState.accepted) { - if (onScrollUpdate != null) - onScrollUpdate(offset); + T delta = _getScrollDelta(event); + if (_state == ScrollState.accepted) { + if (onUpdate != null) + onUpdate(delta); } else { - pendingScrollOffset += offset; - if (pendingScrollOffset.distance > kTouchSlop) + _pendingScrollDelta += delta; + if (_hasSufficientPendingScrollDeltaToAccept) resolve(GestureDisposition.accepted); } } @@ -59,22 +64,64 @@ class ScrollGestureRecognizer extends GestureRecognizer { } void acceptGesture(int pointer) { - if (state != ScrollState.accepted) { - state = ScrollState.accepted; - sky.Offset offset = pendingScrollOffset; - pendingScrollOffset = null; - if (onScrollStart != null) - onScrollStart(); - if (offset != sky.Offset.zero && onScrollUpdate != null) - onScrollUpdate(offset); + if (_state != ScrollState.accepted) { + _state = ScrollState.accepted; + T delta = _pendingScrollDelta; + _pendingScrollDelta = null; + if (onStart != null) + onStart(); + if (delta != _initialPendingScrollDelta && onUpdate != null) + onUpdate(delta); } } void didStopTrackingLastPointer() { - bool wasAccepted = (state == ScrollState.accepted); - state = ScrollState.ready; - if (wasAccepted && onScrollEnd != null) - onScrollEnd(); + bool wasAccepted = (_state == ScrollState.accepted); + _state = ScrollState.ready; + if (wasAccepted && onEnd != null) + onEnd(); + } +} + +class VerticalScrollGestureRecognizer extends _ScrollGestureRecognizer { + VerticalScrollGestureRecognizer({ + PointerRouter router, + GestureScrollStartCallback onStart, + GestureScrollUpdateCallback onUpdate, + GestureScrollEndCallback onEnd + }) : super(router: router, onStart: onStart, onUpdate: onUpdate, onEnd: onEnd); + + double get _initialPendingScrollDelta => 0.0; + // Notice that we negate dy because scroll offsets go in the opposite direction. + double _getScrollDelta(sky.PointerEvent event) => -event.dy; + bool get _hasSufficientPendingScrollDeltaToAccept => _pendingScrollDelta.abs() > kTouchSlop; +} + +class HorizontalScrollGestureRecognizer extends _ScrollGestureRecognizer { + HorizontalScrollGestureRecognizer({ + PointerRouter router, + GestureScrollStartCallback onStart, + GestureScrollUpdateCallback onUpdate, + GestureScrollEndCallback onEnd + }) : super(router: router, onStart: onStart, onUpdate: onUpdate, onEnd: onEnd); + + double get _initialPendingScrollDelta => 0.0; + double _getScrollDelta(sky.PointerEvent event) => -event.dx; + bool get _hasSufficientPendingScrollDeltaToAccept => _pendingScrollDelta.abs() > kTouchSlop; +} + +class PanGestureRecognizer extends _ScrollGestureRecognizer { + PanGestureRecognizer({ + PointerRouter router, + GesturePanStartCallback onStart, + GesturePanUpdateCallback onUpdate, + GesturePanEndCallback onEnd + }) : super(router: router, onStart: onStart, onUpdate: onUpdate, onEnd: onEnd); + + sky.Offset get _initialPendingScrollDelta => sky.Offset.zero; + // Notice that we negate dy because scroll offsets go in the opposite direction. + sky.Offset _getScrollDelta(sky.PointerEvent event) => new sky.Offset(event.dx, -event.dy); + bool get _hasSufficientPendingScrollDeltaToAccept { + return _pendingScrollDelta.dx.abs() > kTouchSlop || _pendingScrollDelta.dy.abs() > kTouchSlop; } - } diff --git a/packages/flutter/lib/widgets/gesture_detector.dart b/packages/flutter/lib/widgets/gesture_detector.dart index 3bffc9b5219..768dad37420 100644 --- a/packages/flutter/lib/widgets/gesture_detector.dart +++ b/packages/flutter/lib/widgets/gesture_detector.dart @@ -19,27 +19,48 @@ class GestureDetector extends StatefulComponent { this.onTap, this.onShowPress, this.onLongPress, - this.onScrollStart, - this.onScrollUpdate, - this.onScrollEnd + this.onVerticalScrollStart, + this.onVerticalScrollUpdate, + this.onVerticalScrollEnd, + this.onHorizontalScrollStart, + this.onHorizontalScrollUpdate, + this.onHorizontalScrollEnd, + this.onPanStart, + this.onPanUpdate, + this.onPanEnd }) : super(key: key); Widget child; GestureTapListener onTap; GestureShowPressListener onShowPress; GestureLongPressListener onLongPress; - GestureScrollStartCallback onScrollStart; - GestureScrollUpdateCallback onScrollUpdate; - GestureScrollEndCallback onScrollEnd; + + GestureScrollStartCallback onVerticalScrollStart; + GestureScrollUpdateCallback onVerticalScrollUpdate; + GestureScrollEndCallback onVerticalScrollEnd; + + GestureScrollStartCallback onHorizontalScrollStart; + GestureScrollUpdateCallback onHorizontalScrollUpdate; + GestureScrollEndCallback onHorizontalScrollEnd; + + GesturePanStartCallback onPanStart; + GesturePanUpdateCallback onPanUpdate; + GesturePanEndCallback onPanEnd; void syncConstructorArguments(GestureDetector source) { child = source.child; onTap = source.onTap; onShowPress = source.onShowPress; onLongPress = source.onLongPress; - onScrollStart = source.onScrollStart; - onScrollUpdate = source.onScrollUpdate; - onScrollEnd = source.onScrollEnd; + onVerticalScrollStart = onVerticalScrollStart; + onVerticalScrollUpdate = onVerticalScrollUpdate; + onVerticalScrollEnd = onVerticalScrollEnd; + onHorizontalScrollStart = onHorizontalScrollStart; + onHorizontalScrollUpdate = onHorizontalScrollUpdate; + onHorizontalScrollEnd = onHorizontalScrollEnd; + onPanStart = source.onPanStart; + onPanUpdate = source.onPanUpdate; + onPanEnd = source.onPanEnd; _syncGestureListeners(); } @@ -66,11 +87,25 @@ class GestureDetector extends StatefulComponent { return _longPress; } - ScrollGestureRecognizer _scroll; - ScrollGestureRecognizer _ensureScroll() { - if (_scroll == null) - _scroll = new ScrollGestureRecognizer(router: _router); - return _scroll; + VerticalScrollGestureRecognizer _verticalScroll; + VerticalScrollGestureRecognizer _ensureVerticalScroll() { + if (_verticalScroll == null) + _verticalScroll = new VerticalScrollGestureRecognizer(router: _router); + return _verticalScroll; + } + + HorizontalScrollGestureRecognizer _horizontalScroll; + HorizontalScrollGestureRecognizer _ensureHorizontalScroll() { + if (_horizontalScroll == null) + _horizontalScroll = new HorizontalScrollGestureRecognizer(router: _router); + return _horizontalScroll; + } + + PanGestureRecognizer _pan; + PanGestureRecognizer _ensurePan() { + if (_pan == null) + _pan = new PanGestureRecognizer(router: _router); + return _pan; } void didMount() { @@ -83,14 +118,18 @@ class GestureDetector extends StatefulComponent { _tap = _ensureDisposed(_tap); _showPress = _ensureDisposed(_showPress); _longPress = _ensureDisposed(_longPress); - _scroll = _ensureDisposed(_scroll); + _verticalScroll = _ensureDisposed(_verticalScroll); + _horizontalScroll = _ensureDisposed(_horizontalScroll); + _pan = _ensureDisposed(_pan); } void _syncGestureListeners() { _syncTap(); _syncShowPress(); _syncLongPress(); - _syncScroll(); + _syncVerticalScroll(); + _syncHorizontalScroll(); + _syncPan(); } void _syncTap() { @@ -114,14 +153,36 @@ class GestureDetector extends StatefulComponent { _ensureLongPress().onLongPress = onLongPress; } - void _syncScroll() { - if (onScrollStart == null && onScrollUpdate == null && onScrollEnd == null) { - _scroll = _ensureDisposed(_scroll); + void _syncVerticalScroll() { + if (onVerticalScrollStart == null && onVerticalScrollUpdate == null && onVerticalScrollEnd == null) { + _verticalScroll = _ensureDisposed(_verticalScroll); } else { - _ensureScroll() - ..onScrollStart = onScrollStart - ..onScrollUpdate = onScrollUpdate - ..onScrollEnd = onScrollEnd; + _ensureVerticalScroll() + ..onStart = onVerticalScrollStart + ..onUpdate = onVerticalScrollUpdate + ..onEnd = onVerticalScrollEnd; + } + } + + void _syncHorizontalScroll() { + if (onHorizontalScrollStart == null && onHorizontalScrollUpdate == null && onHorizontalScrollEnd == null) { + _horizontalScroll = _ensureDisposed(_horizontalScroll); + } else { + _ensureHorizontalScroll() + ..onStart = onHorizontalScrollStart + ..onUpdate = onHorizontalScrollUpdate + ..onEnd = onHorizontalScrollEnd; + } + } + + void _syncPan() { + if (onPanStart == null && onPanUpdate == null && onPanEnd == null) { + _pan = _ensureDisposed(_pan); + } else { + _ensurePan() + ..onStart = onPanStart + ..onUpdate = onPanUpdate + ..onEnd = onPanEnd; } } @@ -138,8 +199,12 @@ class GestureDetector extends StatefulComponent { _showPress.addPointer(event); if (_longPress != null) _longPress.addPointer(event); - if (_scroll != null) - _scroll.addPointer(event); + if (_verticalScroll != null) + _verticalScroll.addPointer(event); + if (_horizontalScroll != null) + _horizontalScroll.addPointer(event); + if (_pan != null) + _pan.addPointer(event); return EventDisposition.processed; } diff --git a/packages/flutter/lib/widgets/scrollable.dart b/packages/flutter/lib/widgets/scrollable.dart index 2f4e2a76918..124b22923f5 100644 --- a/packages/flutter/lib/widgets/scrollable.dart +++ b/packages/flutter/lib/widgets/scrollable.dart @@ -84,8 +84,10 @@ abstract class Scrollable extends StatefulComponent { Widget build() { return new GestureDetector( - onScrollUpdate: _handleScrollUpdate, - onScrollEnd: _maybeSettleScrollOffset, + onVerticalScrollUpdate: scrollDirection == ScrollDirection.vertical ? scrollBy : null, + onVerticalScrollEnd: scrollDirection == ScrollDirection.vertical ? _maybeSettleScrollOffset : null, + onHorizontalScrollUpdate: scrollDirection == ScrollDirection.horizontal ? scrollBy : null, + onHorizontalScrollEnd: scrollDirection == ScrollDirection.horizontal ? _maybeSettleScrollOffset : null, child: new Listener( child: buildContent(), onPointerDown: _handlePointerDown, @@ -172,10 +174,6 @@ abstract class Scrollable extends StatefulComponent { return EventDisposition.processed; } - void _handleScrollUpdate(Offset offset) { - scrollBy(scrollDirection == ScrollDirection.horizontal ? offset.dx : offset.dy); - } - EventDisposition _handleFlingStart(sky.GestureEvent event) { _startToEndAnimation(velocity: _eventVelocity(event)); return EventDisposition.processed; diff --git a/packages/unit/test/gestures/scroll_test.dart b/packages/unit/test/gestures/scroll_test.dart index f56358297f7..8558382944a 100644 --- a/packages/unit/test/gestures/scroll_test.dart +++ b/packages/unit/test/gestures/scroll_test.dart @@ -41,20 +41,20 @@ TestPointerEvent up = new TestPointerEvent( void main() { test('Should recognize scroll', () { PointerRouter router = new PointerRouter(); - ScrollGestureRecognizer scroll = new ScrollGestureRecognizer(router: router); + PanGestureRecognizer scroll = new PanGestureRecognizer(router: router); bool didStartScroll = false; - scroll.onScrollStart = () { + scroll.onStart = () { didStartScroll = true; }; sky.Offset updateOffset; - scroll.onScrollUpdate = (sky.Offset offset) { + scroll.onUpdate = (sky.Offset offset) { updateOffset = offset; }; bool didEndScroll = false; - scroll.onScrollEnd = () { + scroll.onEnd = () { didEndScroll = true; }; diff --git a/packages/unit/test/widget/pageable_list_test.dart b/packages/unit/test/widget/pageable_list_test.dart index 833cfe46cda..5b03254f860 100644 --- a/packages/unit/test/widget/pageable_list_test.dart +++ b/packages/unit/test/widget/pageable_list_test.dart @@ -43,7 +43,7 @@ void main() { expect(currentPage, isNull); new FakeAsync().run((async) { - tester.scroll(tester.findText('1'), new Offset(300.0, 0.0)); + tester.scroll(tester.findText('1'), new Offset(-300.0, 0.0)); // One frame to start the animation, a second to complete it. tester.pumpFrame(builder); tester.pumpFrame(builder, 5000.0);