mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Disambiguate horizontal and vertical scrolling
We now have separate gestures for horizontal, vertical, and pan scrolling.
This commit is contained in:
parent
a5d9c0cb2b
commit
b40864c9b7
@ -15,43 +15,48 @@ enum ScrollState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
typedef void GestureScrollStartCallback();
|
typedef void GestureScrollStartCallback();
|
||||||
typedef void GestureScrollUpdateCallback(sky.Offset scrollDelta);
|
typedef void GestureScrollUpdateCallback(double scrollDelta);
|
||||||
typedef void GestureScrollEndCallback();
|
typedef void GestureScrollEndCallback();
|
||||||
|
|
||||||
sky.Offset _getScrollOffset(sky.PointerEvent event) {
|
typedef void GesturePanStartCallback();
|
||||||
// Notice that we negate dy because scroll offsets go in the opposite direction.
|
typedef void GesturePanUpdateCallback(sky.Offset scrollDelta);
|
||||||
return new sky.Offset(event.dx, -event.dy);
|
typedef void GesturePanEndCallback();
|
||||||
}
|
|
||||||
|
|
||||||
class ScrollGestureRecognizer extends GestureRecognizer {
|
typedef void _GesturePolymorphicUpdateCallback<T>(T scrollDelta);
|
||||||
ScrollGestureRecognizer({ PointerRouter router, this.onScrollStart, this.onScrollUpdate, this.onScrollEnd })
|
|
||||||
|
abstract class _ScrollGestureRecognizer<T extends dynamic> extends GestureRecognizer {
|
||||||
|
_ScrollGestureRecognizer({ PointerRouter router, this.onStart, this.onUpdate, this.onEnd })
|
||||||
: super(router: router);
|
: super(router: router);
|
||||||
|
|
||||||
GestureScrollStartCallback onScrollStart;
|
GestureScrollStartCallback onStart;
|
||||||
GestureScrollUpdateCallback onScrollUpdate;
|
_GesturePolymorphicUpdateCallback<T> onUpdate;
|
||||||
GestureScrollEndCallback onScrollEnd;
|
GestureScrollEndCallback onEnd;
|
||||||
|
|
||||||
ScrollState state = ScrollState.ready;
|
ScrollState _state = ScrollState.ready;
|
||||||
sky.Offset pendingScrollOffset;
|
T _pendingScrollDelta;
|
||||||
|
|
||||||
|
T get _initialPendingScrollDelta;
|
||||||
|
T _getScrollDelta(sky.PointerEvent event);
|
||||||
|
bool get _hasSufficientPendingScrollDeltaToAccept;
|
||||||
|
|
||||||
void addPointer(sky.PointerEvent event) {
|
void addPointer(sky.PointerEvent event) {
|
||||||
startTrackingPointer(event.pointer);
|
startTrackingPointer(event.pointer);
|
||||||
if (state == ScrollState.ready) {
|
if (_state == ScrollState.ready) {
|
||||||
state = ScrollState.possible;
|
_state = ScrollState.possible;
|
||||||
pendingScrollOffset = sky.Offset.zero;
|
_pendingScrollDelta = _initialPendingScrollDelta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleEvent(sky.PointerEvent event) {
|
void handleEvent(sky.PointerEvent event) {
|
||||||
assert(state != ScrollState.ready);
|
assert(_state != ScrollState.ready);
|
||||||
if (event.type == 'pointermove') {
|
if (event.type == 'pointermove') {
|
||||||
sky.Offset offset = _getScrollOffset(event);
|
T delta = _getScrollDelta(event);
|
||||||
if (state == ScrollState.accepted) {
|
if (_state == ScrollState.accepted) {
|
||||||
if (onScrollUpdate != null)
|
if (onUpdate != null)
|
||||||
onScrollUpdate(offset);
|
onUpdate(delta);
|
||||||
} else {
|
} else {
|
||||||
pendingScrollOffset += offset;
|
_pendingScrollDelta += delta;
|
||||||
if (pendingScrollOffset.distance > kTouchSlop)
|
if (_hasSufficientPendingScrollDeltaToAccept)
|
||||||
resolve(GestureDisposition.accepted);
|
resolve(GestureDisposition.accepted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,22 +64,64 @@ class ScrollGestureRecognizer extends GestureRecognizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void acceptGesture(int pointer) {
|
void acceptGesture(int pointer) {
|
||||||
if (state != ScrollState.accepted) {
|
if (_state != ScrollState.accepted) {
|
||||||
state = ScrollState.accepted;
|
_state = ScrollState.accepted;
|
||||||
sky.Offset offset = pendingScrollOffset;
|
T delta = _pendingScrollDelta;
|
||||||
pendingScrollOffset = null;
|
_pendingScrollDelta = null;
|
||||||
if (onScrollStart != null)
|
if (onStart != null)
|
||||||
onScrollStart();
|
onStart();
|
||||||
if (offset != sky.Offset.zero && onScrollUpdate != null)
|
if (delta != _initialPendingScrollDelta && onUpdate != null)
|
||||||
onScrollUpdate(offset);
|
onUpdate(delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void didStopTrackingLastPointer() {
|
void didStopTrackingLastPointer() {
|
||||||
bool wasAccepted = (state == ScrollState.accepted);
|
bool wasAccepted = (_state == ScrollState.accepted);
|
||||||
state = ScrollState.ready;
|
_state = ScrollState.ready;
|
||||||
if (wasAccepted && onScrollEnd != null)
|
if (wasAccepted && onEnd != null)
|
||||||
onScrollEnd();
|
onEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerticalScrollGestureRecognizer extends _ScrollGestureRecognizer<double> {
|
||||||
|
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<double> {
|
||||||
|
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<sky.Offset> {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -19,27 +19,48 @@ class GestureDetector extends StatefulComponent {
|
|||||||
this.onTap,
|
this.onTap,
|
||||||
this.onShowPress,
|
this.onShowPress,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
this.onScrollStart,
|
this.onVerticalScrollStart,
|
||||||
this.onScrollUpdate,
|
this.onVerticalScrollUpdate,
|
||||||
this.onScrollEnd
|
this.onVerticalScrollEnd,
|
||||||
|
this.onHorizontalScrollStart,
|
||||||
|
this.onHorizontalScrollUpdate,
|
||||||
|
this.onHorizontalScrollEnd,
|
||||||
|
this.onPanStart,
|
||||||
|
this.onPanUpdate,
|
||||||
|
this.onPanEnd
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
Widget child;
|
Widget child;
|
||||||
GestureTapListener onTap;
|
GestureTapListener onTap;
|
||||||
GestureShowPressListener onShowPress;
|
GestureShowPressListener onShowPress;
|
||||||
GestureLongPressListener onLongPress;
|
GestureLongPressListener onLongPress;
|
||||||
GestureScrollStartCallback onScrollStart;
|
|
||||||
GestureScrollUpdateCallback onScrollUpdate;
|
GestureScrollStartCallback onVerticalScrollStart;
|
||||||
GestureScrollEndCallback onScrollEnd;
|
GestureScrollUpdateCallback onVerticalScrollUpdate;
|
||||||
|
GestureScrollEndCallback onVerticalScrollEnd;
|
||||||
|
|
||||||
|
GestureScrollStartCallback onHorizontalScrollStart;
|
||||||
|
GestureScrollUpdateCallback onHorizontalScrollUpdate;
|
||||||
|
GestureScrollEndCallback onHorizontalScrollEnd;
|
||||||
|
|
||||||
|
GesturePanStartCallback onPanStart;
|
||||||
|
GesturePanUpdateCallback onPanUpdate;
|
||||||
|
GesturePanEndCallback onPanEnd;
|
||||||
|
|
||||||
void syncConstructorArguments(GestureDetector source) {
|
void syncConstructorArguments(GestureDetector source) {
|
||||||
child = source.child;
|
child = source.child;
|
||||||
onTap = source.onTap;
|
onTap = source.onTap;
|
||||||
onShowPress = source.onShowPress;
|
onShowPress = source.onShowPress;
|
||||||
onLongPress = source.onLongPress;
|
onLongPress = source.onLongPress;
|
||||||
onScrollStart = source.onScrollStart;
|
onVerticalScrollStart = onVerticalScrollStart;
|
||||||
onScrollUpdate = source.onScrollUpdate;
|
onVerticalScrollUpdate = onVerticalScrollUpdate;
|
||||||
onScrollEnd = source.onScrollEnd;
|
onVerticalScrollEnd = onVerticalScrollEnd;
|
||||||
|
onHorizontalScrollStart = onHorizontalScrollStart;
|
||||||
|
onHorizontalScrollUpdate = onHorizontalScrollUpdate;
|
||||||
|
onHorizontalScrollEnd = onHorizontalScrollEnd;
|
||||||
|
onPanStart = source.onPanStart;
|
||||||
|
onPanUpdate = source.onPanUpdate;
|
||||||
|
onPanEnd = source.onPanEnd;
|
||||||
_syncGestureListeners();
|
_syncGestureListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,11 +87,25 @@ class GestureDetector extends StatefulComponent {
|
|||||||
return _longPress;
|
return _longPress;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollGestureRecognizer _scroll;
|
VerticalScrollGestureRecognizer _verticalScroll;
|
||||||
ScrollGestureRecognizer _ensureScroll() {
|
VerticalScrollGestureRecognizer _ensureVerticalScroll() {
|
||||||
if (_scroll == null)
|
if (_verticalScroll == null)
|
||||||
_scroll = new ScrollGestureRecognizer(router: _router);
|
_verticalScroll = new VerticalScrollGestureRecognizer(router: _router);
|
||||||
return _scroll;
|
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() {
|
void didMount() {
|
||||||
@ -83,14 +118,18 @@ class GestureDetector extends StatefulComponent {
|
|||||||
_tap = _ensureDisposed(_tap);
|
_tap = _ensureDisposed(_tap);
|
||||||
_showPress = _ensureDisposed(_showPress);
|
_showPress = _ensureDisposed(_showPress);
|
||||||
_longPress = _ensureDisposed(_longPress);
|
_longPress = _ensureDisposed(_longPress);
|
||||||
_scroll = _ensureDisposed(_scroll);
|
_verticalScroll = _ensureDisposed(_verticalScroll);
|
||||||
|
_horizontalScroll = _ensureDisposed(_horizontalScroll);
|
||||||
|
_pan = _ensureDisposed(_pan);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncGestureListeners() {
|
void _syncGestureListeners() {
|
||||||
_syncTap();
|
_syncTap();
|
||||||
_syncShowPress();
|
_syncShowPress();
|
||||||
_syncLongPress();
|
_syncLongPress();
|
||||||
_syncScroll();
|
_syncVerticalScroll();
|
||||||
|
_syncHorizontalScroll();
|
||||||
|
_syncPan();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncTap() {
|
void _syncTap() {
|
||||||
@ -114,14 +153,36 @@ class GestureDetector extends StatefulComponent {
|
|||||||
_ensureLongPress().onLongPress = onLongPress;
|
_ensureLongPress().onLongPress = onLongPress;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncScroll() {
|
void _syncVerticalScroll() {
|
||||||
if (onScrollStart == null && onScrollUpdate == null && onScrollEnd == null) {
|
if (onVerticalScrollStart == null && onVerticalScrollUpdate == null && onVerticalScrollEnd == null) {
|
||||||
_scroll = _ensureDisposed(_scroll);
|
_verticalScroll = _ensureDisposed(_verticalScroll);
|
||||||
} else {
|
} else {
|
||||||
_ensureScroll()
|
_ensureVerticalScroll()
|
||||||
..onScrollStart = onScrollStart
|
..onStart = onVerticalScrollStart
|
||||||
..onScrollUpdate = onScrollUpdate
|
..onUpdate = onVerticalScrollUpdate
|
||||||
..onScrollEnd = onScrollEnd;
|
..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);
|
_showPress.addPointer(event);
|
||||||
if (_longPress != null)
|
if (_longPress != null)
|
||||||
_longPress.addPointer(event);
|
_longPress.addPointer(event);
|
||||||
if (_scroll != null)
|
if (_verticalScroll != null)
|
||||||
_scroll.addPointer(event);
|
_verticalScroll.addPointer(event);
|
||||||
|
if (_horizontalScroll != null)
|
||||||
|
_horizontalScroll.addPointer(event);
|
||||||
|
if (_pan != null)
|
||||||
|
_pan.addPointer(event);
|
||||||
return EventDisposition.processed;
|
return EventDisposition.processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,8 +84,10 @@ abstract class Scrollable extends StatefulComponent {
|
|||||||
|
|
||||||
Widget build() {
|
Widget build() {
|
||||||
return new GestureDetector(
|
return new GestureDetector(
|
||||||
onScrollUpdate: _handleScrollUpdate,
|
onVerticalScrollUpdate: scrollDirection == ScrollDirection.vertical ? scrollBy : null,
|
||||||
onScrollEnd: _maybeSettleScrollOffset,
|
onVerticalScrollEnd: scrollDirection == ScrollDirection.vertical ? _maybeSettleScrollOffset : null,
|
||||||
|
onHorizontalScrollUpdate: scrollDirection == ScrollDirection.horizontal ? scrollBy : null,
|
||||||
|
onHorizontalScrollEnd: scrollDirection == ScrollDirection.horizontal ? _maybeSettleScrollOffset : null,
|
||||||
child: new Listener(
|
child: new Listener(
|
||||||
child: buildContent(),
|
child: buildContent(),
|
||||||
onPointerDown: _handlePointerDown,
|
onPointerDown: _handlePointerDown,
|
||||||
@ -172,10 +174,6 @@ abstract class Scrollable extends StatefulComponent {
|
|||||||
return EventDisposition.processed;
|
return EventDisposition.processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleScrollUpdate(Offset offset) {
|
|
||||||
scrollBy(scrollDirection == ScrollDirection.horizontal ? offset.dx : offset.dy);
|
|
||||||
}
|
|
||||||
|
|
||||||
EventDisposition _handleFlingStart(sky.GestureEvent event) {
|
EventDisposition _handleFlingStart(sky.GestureEvent event) {
|
||||||
_startToEndAnimation(velocity: _eventVelocity(event));
|
_startToEndAnimation(velocity: _eventVelocity(event));
|
||||||
return EventDisposition.processed;
|
return EventDisposition.processed;
|
||||||
|
@ -41,20 +41,20 @@ TestPointerEvent up = new TestPointerEvent(
|
|||||||
void main() {
|
void main() {
|
||||||
test('Should recognize scroll', () {
|
test('Should recognize scroll', () {
|
||||||
PointerRouter router = new PointerRouter();
|
PointerRouter router = new PointerRouter();
|
||||||
ScrollGestureRecognizer scroll = new ScrollGestureRecognizer(router: router);
|
PanGestureRecognizer scroll = new PanGestureRecognizer(router: router);
|
||||||
|
|
||||||
bool didStartScroll = false;
|
bool didStartScroll = false;
|
||||||
scroll.onScrollStart = () {
|
scroll.onStart = () {
|
||||||
didStartScroll = true;
|
didStartScroll = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
sky.Offset updateOffset;
|
sky.Offset updateOffset;
|
||||||
scroll.onScrollUpdate = (sky.Offset offset) {
|
scroll.onUpdate = (sky.Offset offset) {
|
||||||
updateOffset = offset;
|
updateOffset = offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool didEndScroll = false;
|
bool didEndScroll = false;
|
||||||
scroll.onScrollEnd = () {
|
scroll.onEnd = () {
|
||||||
didEndScroll = true;
|
didEndScroll = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ void main() {
|
|||||||
|
|
||||||
expect(currentPage, isNull);
|
expect(currentPage, isNull);
|
||||||
new FakeAsync().run((async) {
|
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.
|
// One frame to start the animation, a second to complete it.
|
||||||
tester.pumpFrame(builder);
|
tester.pumpFrame(builder);
|
||||||
tester.pumpFrame(builder, 5000.0);
|
tester.pumpFrame(builder, 5000.0);
|
||||||
|
Loading…
Reference in New Issue
Block a user