Unverified Commit 7f02566c authored by Tong Mu's avatar Tong Mu Committed by GitHub

Add "OneSequenceRecognizer.resolvePointer". Fix DragGestureRecognizer crash on...

Add "OneSequenceRecognizer.resolvePointer". Fix DragGestureRecognizer crash on multiple pointers (#39017)

* Add drag tests

* Add resolvePointer

* Correct monodrag implementation

* Fix some addTearDown
parent 74076958
......@@ -251,8 +251,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
if (event is PointerMoveEvent) {
if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(event.pointer);
_giveUpPointer(event.pointer);
return;
}
if (_state == _DragState.accepted) {
......@@ -278,7 +277,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
resolve(GestureDisposition.accepted);
}
}
stopTrackingIfPointerNoLongerDown(event);
if (event is PointerUpEvent || event is PointerCancelEvent) {
_giveUpPointer(
event.pointer,
reject: event is PointerCancelEvent || _state ==_DragState.possible,
);
}
}
@override
......@@ -325,7 +329,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
@override
void rejectGesture(int pointer) {
stopTrackingPointer(pointer);
_giveUpPointer(pointer);
}
@override
......@@ -349,6 +353,16 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
_state = _DragState.ready;
}
void _giveUpPointer(int pointer, {bool reject = true}) {
stopTrackingPointer(pointer);
if (reject) {
if (_velocityTrackers.containsKey(pointer)) {
_velocityTrackers.remove(pointer);
resolvePointer(pointer, GestureDisposition.rejected);
}
}
}
void _checkDown() {
assert(_initialButtons == kPrimaryButton);
final DragDownDetails details = DragDownDetails(
......
......@@ -255,6 +255,18 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
entry.resolve(disposition);
}
/// Resolves this recognizer's participation in the given gesture arena with
/// the given disposition.
@protected
@mustCallSuper
void resolvePointer(int pointer, GestureDisposition disposition) {
final GestureArenaEntry entry = _entries[pointer];
if (entry != null) {
entry.resolve(disposition);
_entries.remove(pointer);
}
}
@override
void dispose() {
resolve(GestureDisposition.rejected);
......
......@@ -14,6 +14,8 @@ void main() {
testGesture('Should recognize pan', (GestureTester tester) {
final PanGestureRecognizer pan = PanGestureRecognizer();
final TapGestureRecognizer tap = TapGestureRecognizer()..onTap = () {};
addTearDown(pan.dispose);
addTearDown(tap.dispose);
bool didStartPan = false;
pan.onStart = (_) {
......@@ -74,15 +76,14 @@ void main() {
expect(didEndPan, isTrue);
didEndPan = false;
expect(didTap, isFalse);
pan.dispose();
tap.dispose();
});
testGesture('Should report most recent point to onStart by default', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
..onStart = (_) {};
addTearDown(() => drag?.dispose);
addTearDown(() => competingDrag?.dispose);
Offset positionAtOnStart;
drag.onStart = (DragStartDetails details) {
......@@ -98,15 +99,19 @@ void main() {
tester.route(pointer.move(const Offset(30.0, 0.0)));
drag.dispose();
drag = null;
competingDrag.dispose();
competingDrag = null;
expect(positionAtOnStart, const Offset(30.0, 00.0));
});
testGesture('Should report most recent point to onStart with a start configuration', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
..onStart = (_) {};
addTearDown(() => drag?.dispose);
addTearDown(() => competingDrag?.dispose);
Offset positionAtOnStart;
drag.onStart = (DragStartDetails details) {
......@@ -126,7 +131,9 @@ void main() {
tester.route(pointer.move(const Offset(30.0, 0.0)));
drag.dispose();
drag = null;
competingDrag.dispose();
competingDrag = null;
expect(positionAtOnStart, const Offset(30.0, 0.0));
expect(updateOffset, null);
......@@ -134,6 +141,7 @@ void main() {
testGesture('Should recognize drag', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
bool didStartDrag = false;
drag.onStart = (_) {
......@@ -181,12 +189,11 @@ void main() {
expect(updatedDelta, isNull);
expect(didEndDrag, isTrue);
didEndDrag = false;
drag.dispose();
});
testGesture('Should report original timestamps', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
Duration startTimestamp;
drag.onStart = (DragStartDetails details) {
......@@ -212,18 +219,16 @@ void main() {
tester.route(pointer.move(const Offset(20.0, 25.0), timeStamp: const Duration(milliseconds: 300)));
expect(updatedTimestamp, const Duration(milliseconds: 300));
drag.dispose();
});
// TODO(jslavitz): Revert these tests.
testGesture('Should report initial down point to onStart with a down configuration', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer()
HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer()
..dragStartBehavior = DragStartBehavior.down;
final VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
VerticalDragGestureRecognizer competingDrag = VerticalDragGestureRecognizer()
..dragStartBehavior = DragStartBehavior.down
..onStart = (_) {};
addTearDown(() => drag?.dispose);
addTearDown(() => competingDrag?.dispose);
Offset positionAtOnStart;
drag.onStart = (DragStartDetails details) {
......@@ -245,7 +250,9 @@ void main() {
tester.route(pointer.move(const Offset(30.0, 0.0)));
drag.dispose();
drag = null;
competingDrag.dispose();
competingDrag = null;
expect(positionAtOnStart, const Offset(10.0, 10.0));
......@@ -256,10 +263,12 @@ void main() {
});
testGesture('Drag with multiple pointers in down behavior', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag1 =
HorizontalDragGestureRecognizer drag1 =
HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
final VerticalDragGestureRecognizer drag2 =
VerticalDragGestureRecognizer drag2 =
VerticalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(() => drag1?.dispose);
addTearDown(() => drag2?.dispose);
final List<String> log = <String>[];
drag1.onDown = (_) { log.add('drag1-down'); };
......@@ -303,7 +312,9 @@ void main() {
tester.route(pointer6.up());
drag1.dispose();
drag1 = null;
drag2.dispose();
drag2 = null;
expect(log, <String>[
'drag1-down',
......@@ -328,6 +339,7 @@ void main() {
testGesture('Clamp max velocity', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
Velocity velocity;
double primaryVelocity;
......@@ -356,12 +368,11 @@ void main() {
expect(velocity.pixelsPerSecond.dx, inInclusiveRange(0.99 * kMaxFlingVelocity, kMaxFlingVelocity));
expect(velocity.pixelsPerSecond.dy, moreOrLessEquals(0.0));
expect(primaryVelocity, velocity.pixelsPerSecond.dx);
drag.dispose();
});
testGesture('Synthesized pointer events are ignored for velocity tracking', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
Velocity velocity;
drag.onEnd = (DragEndDetails details) {
......@@ -388,14 +399,13 @@ void main() {
tester.route(pointer.up(timeStamp: const Duration(milliseconds: 70)));
expect(velocity.pixelsPerSecond.dx, moreOrLessEquals(1000.0));
expect(velocity.pixelsPerSecond.dy, moreOrLessEquals(0.0));
drag.dispose();
});
/// Checks that quick flick gestures with 1 down, 2 move and 1 up pointer
/// events still have a velocity
testGesture('Quick flicks have velocity', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
Velocity velocity;
drag.onEnd = (DragEndDetails details) {
......@@ -413,8 +423,6 @@ void main() {
// 3 events moving by 10px every 10ms = 1000px/s.
expect(velocity.pixelsPerSecond.dx, moreOrLessEquals(1000.0));
expect(velocity.pixelsPerSecond.dy, moreOrLessEquals(0.0));
drag.dispose();
});
testGesture('Drag details', (GestureTester tester) {
......@@ -426,6 +434,7 @@ void main() {
testGesture('Should recognize drag', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
bool didStartDrag = false;
drag.onStart = (_) {
......@@ -483,12 +492,11 @@ void main() {
expect(updatePrimaryDelta, isNull);
expect(didEndDrag, isTrue);
didEndDrag = false;
drag.dispose();
});
testGesture('Should recognize drag', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
Offset latestGlobalPosition;
drag.onStart = (DragStartDetails details) {
......@@ -518,7 +526,6 @@ void main() {
expect(latestDelta, const Offset(-20.0, 0.0));
tester.route(pointer.up());
drag.dispose();
});
testGesture('Can filter drags based on device kind', (GestureTester tester) {
......@@ -527,6 +534,7 @@ void main() {
kind: PointerDeviceKind.mouse,
)
..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
bool didStartDrag = false;
drag.onStart = (_) {
......@@ -596,8 +604,6 @@ void main() {
expect(updatedDelta, isNull);
expect(didEndDrag, isTrue);
didEndDrag = false;
drag.dispose();
});
group('Enforce consistent-button restriction:', () {
......@@ -822,6 +828,8 @@ void main() {
..onCancel = () {
recognized.add('primaryCancel');
};
addTearDown(pan.dispose);
addTearDown(tap.dispose);
final TestPointer pointer = TestPointer(
5,
......@@ -840,9 +848,312 @@ void main() {
tester.route(pointer.up());
expect(recognized, <String>[]);
recognized.clear();
});
pan.dispose();
tap.dispose();
testGesture('A secondary drag should not trigger primary', (GestureTester tester) {
final List<String> recognized = <String>[];
final TapGestureRecognizer tap = TapGestureRecognizer()
..onTap = () {}; // Need a listener to enable competition.
final PanGestureRecognizer pan = PanGestureRecognizer()
..onDown = (DragDownDetails details) {
recognized.add('primaryDown');
}
..onStart = (DragStartDetails details) {
recognized.add('primaryStart');
}
..onUpdate = (DragUpdateDetails details) {
recognized.add('primaryUpdate');
}
..onEnd = (DragEndDetails details) {
recognized.add('primaryEnd');
}
..onCancel = () {
recognized.add('primaryCancel');
};
final TestPointer pointer = TestPointer(
5,
PointerDeviceKind.touch,
0,
kSecondaryButton,
);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
pan.addPointer(down);
tap.addPointer(down);
tester.closeArena(5);
tester.route(down);
tester.route(pointer.move(const Offset(20.0, 30.0)));
tester.route(pointer.move(const Offset(20.0, 25.0)));
tester.route(pointer.up());
expect(recognized, <String>[]);
recognized.clear();
addTearDown(pan.dispose);
addTearDown(tap.dispose);
recognized.clear();
});
testGesture('On multiple pointers, DragGestureRecognizer is canceled '
'when all pointers are canceled (FIFO)', (GestureTester tester) {
// This test simulates the following scenario:
// P1 down, P2 down, P1 up, P2 up
final List<String> logs = <String>[];
final HorizontalDragGestureRecognizer hori = HorizontalDragGestureRecognizer()
..onDown = (DragDownDetails details) { logs.add('downH'); }
..onStart = (DragStartDetails details) { logs.add('startH'); }
..onUpdate = (DragUpdateDetails details) { logs.add('updateH'); }
..onEnd = (DragEndDetails details) { logs.add('endH'); }
..onCancel = () { logs.add('cancelH'); };
// Competitor
final TapGestureRecognizer vert = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { logs.add('downT'); }
..onTapUp = (TapUpDetails details) { logs.add('upT'); }
..onTapCancel = () {};
addTearDown(hori.dispose);
addTearDown(vert.dispose);
final TestPointer pointer1 = TestPointer(4, PointerDeviceKind.touch);
final TestPointer pointer2 = TestPointer(5, PointerDeviceKind.touch);
final PointerDownEvent down1 = pointer1.down(const Offset(10.0, 10.0));
final PointerDownEvent down2 = pointer2.down(const Offset(11.0, 10.0));
hori.addPointer(down1);
vert.addPointer(down1);
tester.route(down1);
tester.closeArena(pointer1.pointer);
expect(logs, <String>['downH']);
logs.clear();
hori.addPointer(down2);
vert.addPointer(down2);
tester.route(down2);
tester.closeArena(pointer2.pointer);
expect(logs, <String>[]);
logs.clear();
tester.route(pointer1.up());
GestureBinding.instance.gestureArena.sweep(pointer1.pointer);
expect(logs, <String>['downT', 'upT']);
logs.clear();
tester.route(pointer2.up());
GestureBinding.instance.gestureArena.sweep(pointer2.pointer);
expect(logs, <String>['cancelH']);
logs.clear();
});
testGesture('On multiple pointers, DragGestureRecognizer is canceled '
'when all pointers are canceled (FILO)', (GestureTester tester) {
// This test simulates the following scenario:
// P1 down, P2 down, P1 up, P2 up
final List<String> logs = <String>[];
final HorizontalDragGestureRecognizer hori = HorizontalDragGestureRecognizer()
..onDown = (DragDownDetails details) { logs.add('downH'); }
..onStart = (DragStartDetails details) { logs.add('startH'); }
..onUpdate = (DragUpdateDetails details) { logs.add('updateH'); }
..onEnd = (DragEndDetails details) { logs.add('endH'); }
..onCancel = () { logs.add('cancelH'); };
// Competitor
final TapGestureRecognizer vert = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { logs.add('downT'); }
..onTapUp = (TapUpDetails details) { logs.add('upT'); }
..onTapCancel = () {};
addTearDown(hori.dispose);
addTearDown(vert.dispose);
final TestPointer pointer1 = TestPointer(4, PointerDeviceKind.touch);
final TestPointer pointer2 = TestPointer(5, PointerDeviceKind.touch);
final PointerDownEvent down1 = pointer1.down(const Offset(10.0, 10.0));
final PointerDownEvent down2 = pointer2.down(const Offset(11.0, 10.0));
hori.addPointer(down1);
vert.addPointer(down1);
tester.route(down1);
tester.closeArena(pointer1.pointer);
expect(logs, <String>['downH']);
logs.clear();
hori.addPointer(down2);
vert.addPointer(down2);
tester.route(down2);
tester.closeArena(pointer2.pointer);
expect(logs, <String>[]);
logs.clear();
tester.route(pointer2.up());
GestureBinding.instance.gestureArena.sweep(pointer2.pointer);
// Tap is not triggered because pointer2 is not its primary pointer
expect(logs, <String>[]);
logs.clear();
tester.route(pointer1.up());
GestureBinding.instance.gestureArena.sweep(pointer1.pointer);
expect(logs, <String>['cancelH', 'downT', 'upT']);
logs.clear();
});
testGesture('On multiple pointers, DragGestureRecognizer is accepted when the '
'first pointer is accepted', (GestureTester tester) {
// This test simulates the following scenario:
// P1 down, P2 down, P1 moves away, P2 up
final List<String> logs = <String>[];
final HorizontalDragGestureRecognizer hori = HorizontalDragGestureRecognizer()
..onDown = (DragDownDetails details) { logs.add('downH'); }
..onStart = (DragStartDetails details) { logs.add('startH'); }
..onUpdate = (DragUpdateDetails details) { logs.add('updateH'); }
..onEnd = (DragEndDetails details) { logs.add('endH'); }
..onCancel = () { logs.add('cancelH'); };
// Competitor
final TapGestureRecognizer vert = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { logs.add('downT'); }
..onTapUp = (TapUpDetails details) { logs.add('upT'); }
..onTapCancel = () {};
addTearDown(hori.dispose);
addTearDown(vert.dispose);
final TestPointer pointer1 = TestPointer(4, PointerDeviceKind.touch);
final TestPointer pointer2 = TestPointer(5, PointerDeviceKind.touch);
final PointerDownEvent down1 = pointer1.down(const Offset(10.0, 10.0));
final PointerDownEvent down2 = pointer2.down(const Offset(11.0, 10.0));
hori.addPointer(down1);
vert.addPointer(down1);
tester.route(down1);
tester.closeArena(pointer1.pointer);
expect(logs, <String>['downH']);
logs.clear();
hori.addPointer(down2);
vert.addPointer(down2);
tester.route(down2);
tester.closeArena(pointer2.pointer);
expect(logs, <String>[]);
logs.clear();
tester.route(pointer1.move(const Offset(100, 100)));
expect(logs, <String>['startH']);
logs.clear();
tester.route(pointer2.up());
GestureBinding.instance.gestureArena.sweep(pointer2.pointer);
expect(logs, <String>[]);
logs.clear();
tester.route(pointer1.up());
GestureBinding.instance.gestureArena.sweep(pointer1.pointer);
expect(logs, <String>['endH']);
logs.clear();
});
testGesture('On multiple pointers, canceled pointers (due to up) do not '
'prevent later pointers getting accepted', (GestureTester tester) {
// This test simulates the following scenario:
// P1 down, P2 down, P1 Up, P2 moves away
final List<String> logs = <String>[];
final HorizontalDragGestureRecognizer hori = HorizontalDragGestureRecognizer()
..onDown = (DragDownDetails details) { logs.add('downH'); }
..onStart = (DragStartDetails details) { logs.add('startH'); }
..onUpdate = (DragUpdateDetails details) { logs.add('updateH'); }
..onEnd = (DragEndDetails details) { logs.add('endH'); }
..onCancel = () { logs.add('cancelH'); };
// Competitor
final TapGestureRecognizer vert = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { logs.add('downT'); }
..onTapUp = (TapUpDetails details) { logs.add('upT'); }
..onTapCancel = () {};
addTearDown(hori.dispose);
addTearDown(vert.dispose);
final TestPointer pointer1 = TestPointer(4, PointerDeviceKind.touch);
final TestPointer pointer2 = TestPointer(5, PointerDeviceKind.touch);
final PointerDownEvent down1 = pointer1.down(const Offset(10.0, 10.0));
final PointerDownEvent down2 = pointer2.down(const Offset(11.0, 10.0));
hori.addPointer(down1);
vert.addPointer(down1);
tester.route(down1);
tester.closeArena(pointer1.pointer);
expect(logs, <String>['downH']);
logs.clear();
hori.addPointer(down2);
vert.addPointer(down2);
tester.route(down2);
tester.closeArena(pointer2.pointer);
expect(logs, <String>[]);
logs.clear();
tester.route(pointer1.up());
GestureBinding.instance.gestureArena.sweep(pointer1.pointer);
expect(logs, <String>['downT', 'upT']);
logs.clear();
tester.route(pointer2.move(const Offset(100, 100)));
expect(logs, <String>['startH']);
logs.clear();
tester.route(pointer2.up());
GestureBinding.instance.gestureArena.sweep(pointer2.pointer);
expect(logs, <String>['endH']);
logs.clear();
});
testGesture('On multiple pointers, canceled pointers (due to buttons) do not '
'prevent later pointers getting accepted', (GestureTester tester) {
// This test simulates the following scenario:
// P1 down, P2 down, P1 change buttons, P2 moves away
final List<String> logs = <String>[];
final HorizontalDragGestureRecognizer hori = HorizontalDragGestureRecognizer()
..onDown = (DragDownDetails details) { logs.add('downH'); }
..onStart = (DragStartDetails details) { logs.add('startH'); }
..onUpdate = (DragUpdateDetails details) { logs.add('updateH'); }
..onEnd = (DragEndDetails details) { logs.add('endH'); }
..onCancel = () { logs.add('cancelH'); };
// Competitor
final TapGestureRecognizer vert = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { logs.add('downT'); }
..onTapUp = (TapUpDetails details) { logs.add('upT'); }
..onTapCancel = () {};
addTearDown(hori.dispose);
addTearDown(vert.dispose);
final TestPointer pointer1 = TestPointer(1, PointerDeviceKind.touch);
final TestPointer pointer2 = TestPointer(2, PointerDeviceKind.touch);
final PointerDownEvent down1 = pointer1.down(const Offset(10.0, 10.0));
final PointerDownEvent down2 = pointer2.down(const Offset(11.0, 10.0));
hori.addPointer(down1);
vert.addPointer(down1);
tester.route(down1);
tester.closeArena(pointer1.pointer);
hori.addPointer(down2);
vert.addPointer(down2);
tester.route(down2);
tester.closeArena(pointer2.pointer);
expect(logs, <String>['downH']);
logs.clear();
// Pointer 1 changes buttons, which cancel tap, leaving drag the only
// remaining member of arena 1, therefore drag is accepted.
tester.route(pointer1.move(const Offset(9.9, 9.9), buttons: kSecondaryButton));
expect(logs, <String>['startH']);
logs.clear();
tester.route(pointer2.move(const Offset(100, 100)));
expect(logs, <String>['updateH']);
logs.clear();
tester.route(pointer2.up());
GestureBinding.instance.gestureArena.sweep(pointer2.pointer);
expect(logs, <String>['endH']);
logs.clear();
});
}
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