1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/gestures.dart'; |
7 | import 'package:flutter_test/flutter_test.dart'; |
8 | |
9 | import '../gestures/gesture_tester.dart'; |
10 | |
11 | // Anything longer than [kDoubleTapTimeout] will reset the consecutive tap count. |
12 | final Duration kConsecutiveTapDelay = kDoubleTapTimeout ~/ 2; |
13 | |
14 | void main() { |
15 | TestWidgetsFlutterBinding.ensureInitialized(); |
16 | |
17 | late List<String> events; |
18 | late BaseTapAndDragGestureRecognizer tapAndDrag; |
19 | |
20 | void setUpTapAndPanGestureRecognizer({ |
21 | bool eagerVictoryOnDrag = true, // This is the default for [BaseTapAndDragGestureRecognizer]. |
22 | }) { |
23 | tapAndDrag = |
24 | TapAndPanGestureRecognizer() |
25 | ..dragStartBehavior = DragStartBehavior.down |
26 | ..eagerVictoryOnDrag = eagerVictoryOnDrag |
27 | ..maxConsecutiveTap = 3 |
28 | ..onTapDown = (TapDragDownDetails details) { |
29 | events.add('down# ${details.consecutiveTapCount}' ); |
30 | } |
31 | ..onTapUp = (TapDragUpDetails details) { |
32 | events.add('up# ${details.consecutiveTapCount}' ); |
33 | } |
34 | ..onDragStart = (TapDragStartDetails details) { |
35 | events.add('panstart# ${details.consecutiveTapCount}' ); |
36 | } |
37 | ..onDragUpdate = (TapDragUpdateDetails details) { |
38 | events.add('panupdate# ${details.consecutiveTapCount}' ); |
39 | } |
40 | ..onDragEnd = (TapDragEndDetails details) { |
41 | events.add('panend# ${details.consecutiveTapCount}' ); |
42 | } |
43 | ..onCancel = () { |
44 | events.add('cancel' ); |
45 | }; |
46 | addTearDown(tapAndDrag.dispose); |
47 | } |
48 | |
49 | void setUpTapAndHorizontalDragGestureRecognizer({ |
50 | bool eagerVictoryOnDrag = true, // This is the default for [BaseTapAndDragGestureRecognizer]. |
51 | }) { |
52 | tapAndDrag = |
53 | TapAndHorizontalDragGestureRecognizer() |
54 | ..dragStartBehavior = DragStartBehavior.down |
55 | ..eagerVictoryOnDrag = eagerVictoryOnDrag |
56 | ..maxConsecutiveTap = 3 |
57 | ..onTapDown = (TapDragDownDetails details) { |
58 | events.add('down# ${details.consecutiveTapCount}' ); |
59 | } |
60 | ..onTapUp = (TapDragUpDetails details) { |
61 | events.add('up# ${details.consecutiveTapCount}' ); |
62 | } |
63 | ..onDragStart = (TapDragStartDetails details) { |
64 | events.add('horizontaldragstart# ${details.consecutiveTapCount}' ); |
65 | } |
66 | ..onDragUpdate = (TapDragUpdateDetails details) { |
67 | events.add('horizontaldragupdate# ${details.consecutiveTapCount}' ); |
68 | } |
69 | ..onDragEnd = (TapDragEndDetails details) { |
70 | events.add('horizontaldragend# ${details.consecutiveTapCount}' ); |
71 | } |
72 | ..onCancel = () { |
73 | events.add('cancel' ); |
74 | }; |
75 | addTearDown(tapAndDrag.dispose); |
76 | } |
77 | |
78 | setUp(() { |
79 | events = <String>[]; |
80 | }); |
81 | |
82 | // Down/up pair 1: normal tap sequence |
83 | const PointerDownEvent down1 = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0)); |
84 | |
85 | const PointerUpEvent up1 = PointerUpEvent(pointer: 1, position: Offset(11.0, 9.0)); |
86 | |
87 | const PointerCancelEvent cancel1 = PointerCancelEvent(pointer: 1); |
88 | |
89 | // Down/up pair 2: normal tap sequence close to pair 1 |
90 | const PointerDownEvent down2 = PointerDownEvent(pointer: 2, position: Offset(12.0, 12.0)); |
91 | |
92 | const PointerUpEvent up2 = PointerUpEvent(pointer: 2, position: Offset(13.0, 11.0)); |
93 | |
94 | // Down/up pair 3: normal tap sequence close to pair 1 |
95 | const PointerDownEvent down3 = PointerDownEvent(pointer: 3, position: Offset(12.0, 12.0)); |
96 | |
97 | const PointerUpEvent up3 = PointerUpEvent(pointer: 3, position: Offset(13.0, 11.0)); |
98 | |
99 | // Down/up pair 4: normal tap sequence far away from pair 1 |
100 | const PointerDownEvent down4 = PointerDownEvent(pointer: 4, position: Offset(130.0, 130.0)); |
101 | |
102 | const PointerUpEvent up4 = PointerUpEvent(pointer: 4, position: Offset(131.0, 129.0)); |
103 | |
104 | // Down/move/up sequence 5: intervening motion |
105 | const PointerDownEvent down5 = PointerDownEvent(pointer: 5, position: Offset(10.0, 10.0)); |
106 | |
107 | const PointerMoveEvent move5 = PointerMoveEvent(pointer: 5, position: Offset(25.0, 25.0)); |
108 | |
109 | const PointerUpEvent up5 = PointerUpEvent(pointer: 5, position: Offset(25.0, 25.0)); |
110 | |
111 | // Mouse Down/move/up sequence 6: intervening motion - kPrecisePointerPanSlop |
112 | const PointerDownEvent down6 = PointerDownEvent( |
113 | kind: PointerDeviceKind.mouse, |
114 | pointer: 6, |
115 | position: Offset(10.0, 10.0), |
116 | ); |
117 | |
118 | const PointerMoveEvent move6 = PointerMoveEvent( |
119 | kind: PointerDeviceKind.mouse, |
120 | pointer: 6, |
121 | position: Offset(15.0, 15.0), |
122 | delta: Offset(5.0, 5.0), |
123 | ); |
124 | |
125 | const PointerUpEvent up6 = PointerUpEvent( |
126 | kind: PointerDeviceKind.mouse, |
127 | pointer: 6, |
128 | position: Offset(15.0, 15.0), |
129 | ); |
130 | |
131 | testGesture('Recognizes consecutive taps' , (GestureTester tester) { |
132 | setUpTapAndPanGestureRecognizer(); |
133 | |
134 | tapAndDrag.addPointer(down1); |
135 | tester.closeArena(1); |
136 | tester.route(down1); |
137 | tester.route(up1); |
138 | GestureBinding.instance.gestureArena.sweep(1); |
139 | expect(events, <String>['down#1' , 'up#1' ]); |
140 | |
141 | events.clear(); |
142 | tester.async.elapse(kConsecutiveTapDelay); |
143 | tapAndDrag.addPointer(down2); |
144 | tester.closeArena(2); |
145 | tester.route(down2); |
146 | tester.route(up2); |
147 | GestureBinding.instance.gestureArena.sweep(2); |
148 | expect(events, <String>['down#2' , 'up#2' ]); |
149 | |
150 | events.clear(); |
151 | tester.async.elapse(kConsecutiveTapDelay); |
152 | tapAndDrag.addPointer(down3); |
153 | tester.closeArena(3); |
154 | tester.route(down3); |
155 | tester.route(up3); |
156 | GestureBinding.instance.gestureArena.sweep(3); |
157 | expect(events, <String>['down#3' , 'up#3' ]); |
158 | }); |
159 | |
160 | testGesture('Resets if times out in between taps' , (GestureTester tester) { |
161 | setUpTapAndPanGestureRecognizer(); |
162 | |
163 | tapAndDrag.addPointer(down1); |
164 | tester.closeArena(1); |
165 | tester.route(down1); |
166 | tester.route(up1); |
167 | GestureBinding.instance.gestureArena.sweep(1); |
168 | expect(events, <String>['down#1' , 'up#1' ]); |
169 | |
170 | events.clear(); |
171 | tester.async.elapse(const Duration(milliseconds: 1000)); |
172 | tapAndDrag.addPointer(down2); |
173 | tester.closeArena(2); |
174 | tester.route(down2); |
175 | tester.route(up2); |
176 | GestureBinding.instance.gestureArena.sweep(2); |
177 | expect(events, <String>['down#1' , 'up#1' ]); |
178 | }); |
179 | |
180 | testGesture('Resets if taps are far apart' , (GestureTester tester) { |
181 | setUpTapAndPanGestureRecognizer(); |
182 | |
183 | tapAndDrag.addPointer(down1); |
184 | tester.closeArena(1); |
185 | tester.route(down1); |
186 | tester.route(up1); |
187 | GestureBinding.instance.gestureArena.sweep(1); |
188 | expect(events, <String>['down#1' , 'up#1' ]); |
189 | |
190 | events.clear(); |
191 | tester.async.elapse(const Duration(milliseconds: 100)); |
192 | tapAndDrag.addPointer(down4); |
193 | tester.closeArena(4); |
194 | tester.route(down4); |
195 | tester.route(up4); |
196 | GestureBinding.instance.gestureArena.sweep(4); |
197 | expect(events, <String>['down#1' , 'up#1' ]); |
198 | }); |
199 | |
200 | testGesture('Resets if consecutiveTapCount reaches maxConsecutiveTap' , (GestureTester tester) { |
201 | setUpTapAndPanGestureRecognizer(); |
202 | |
203 | // First tap. |
204 | tapAndDrag.addPointer(down1); |
205 | tester.closeArena(1); |
206 | tester.route(down1); |
207 | tester.route(up1); |
208 | GestureBinding.instance.gestureArena.sweep(1); |
209 | expect(events, <String>['down#1' , 'up#1' ]); |
210 | |
211 | // Second tap. |
212 | events.clear(); |
213 | tapAndDrag.addPointer(down2); |
214 | tester.closeArena(2); |
215 | tester.route(down2); |
216 | tester.route(up2); |
217 | GestureBinding.instance.gestureArena.sweep(2); |
218 | expect(events, <String>['down#2' , 'up#2' ]); |
219 | |
220 | // Third tap. |
221 | events.clear(); |
222 | tapAndDrag.addPointer(down3); |
223 | tester.closeArena(3); |
224 | tester.route(down3); |
225 | tester.route(up3); |
226 | GestureBinding.instance.gestureArena.sweep(3); |
227 | expect(events, <String>['down#3' , 'up#3' ]); |
228 | |
229 | // Fourth tap. Here we arrived at the `maxConsecutiveTap` for `consecutiveTapCount` |
230 | // so our count should reset and our new count should be `1`. |
231 | events.clear(); |
232 | tapAndDrag.addPointer(down3); |
233 | tester.closeArena(3); |
234 | tester.route(down3); |
235 | tester.route(up3); |
236 | GestureBinding.instance.gestureArena.sweep(3); |
237 | expect(events, <String>['down#1' , 'up#1' ]); |
238 | }); |
239 | |
240 | testGesture('Should recognize drag' , (GestureTester tester) { |
241 | setUpTapAndPanGestureRecognizer(); |
242 | |
243 | final TestPointer pointer = TestPointer(5); |
244 | final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0)); |
245 | tapAndDrag.addPointer(down); |
246 | tester.closeArena(5); |
247 | tester.route(down); |
248 | tester.route(pointer.move(const Offset(40.0, 45.0))); |
249 | tester.route(pointer.up()); |
250 | GestureBinding.instance.gestureArena.sweep(5); |
251 | expect(events, <String>['down#1' , 'panstart#1' , 'panupdate#1' , 'panend#1' ]); |
252 | }); |
253 | |
254 | testGesture('Recognizes consecutive taps + drag' , (GestureTester tester) { |
255 | setUpTapAndPanGestureRecognizer(); |
256 | |
257 | final TestPointer pointer = TestPointer(5); |
258 | final PointerDownEvent downA = pointer.down(const Offset(10.0, 10.0)); |
259 | tapAndDrag.addPointer(downA); |
260 | tester.closeArena(5); |
261 | tester.route(downA); |
262 | tester.route(pointer.up()); |
263 | GestureBinding.instance.gestureArena.sweep(5); |
264 | |
265 | tester.async.elapse(kConsecutiveTapDelay); |
266 | |
267 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
268 | tapAndDrag.addPointer(downB); |
269 | tester.closeArena(5); |
270 | tester.route(downB); |
271 | tester.route(pointer.up()); |
272 | GestureBinding.instance.gestureArena.sweep(5); |
273 | |
274 | tester.async.elapse(kConsecutiveTapDelay); |
275 | |
276 | final PointerDownEvent downC = pointer.down(const Offset(10.0, 10.0)); |
277 | tapAndDrag.addPointer(downC); |
278 | tester.closeArena(5); |
279 | tester.route(downC); |
280 | tester.route(pointer.move(const Offset(40.0, 45.0))); |
281 | tester.route(pointer.up()); |
282 | expect(events, <String>[ |
283 | 'down#1' , |
284 | 'up#1' , |
285 | 'down#2' , |
286 | 'up#2' , |
287 | 'down#3' , |
288 | 'panstart#3' , |
289 | 'panupdate#3' , |
290 | 'panend#3' , |
291 | ]); |
292 | }); |
293 | |
294 | testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - before acceptance' , ( |
295 | GestureTester tester, |
296 | ) { |
297 | setUpTapAndPanGestureRecognizer(); |
298 | |
299 | tapAndDrag.addPointer(down1); |
300 | tapAndDrag.addPointer(down2); |
301 | tester.closeArena(1); |
302 | tester.route(down1); |
303 | |
304 | tester.closeArena(2); |
305 | tester.route(down2); |
306 | |
307 | tester.route(up1); |
308 | GestureBinding.instance.gestureArena.sweep(1); |
309 | |
310 | tester.route(up2); |
311 | GestureBinding.instance.gestureArena.sweep(2); |
312 | expect(events, <String>['down#1' , 'up#1' ]); |
313 | }); |
314 | |
315 | testGesture('Calls tap up when the recognizer accepts before handleEvent is called' , ( |
316 | GestureTester tester, |
317 | ) { |
318 | setUpTapAndPanGestureRecognizer(); |
319 | |
320 | tapAndDrag.addPointer(down1); |
321 | tester.closeArena(1); |
322 | GestureBinding.instance.gestureArena.sweep(1); |
323 | tester.route(down1); |
324 | tester.route(up1); |
325 | expect(events, <String>['down#1' , 'up#1' ]); |
326 | }); |
327 | |
328 | testGesture('Recognizer rejects pointer that is not the primary one (FILO) - before acceptance' , ( |
329 | GestureTester tester, |
330 | ) { |
331 | setUpTapAndPanGestureRecognizer(); |
332 | |
333 | tapAndDrag.addPointer(down1); |
334 | tapAndDrag.addPointer(down2); |
335 | tester.closeArena(1); |
336 | tester.route(down1); |
337 | |
338 | tester.closeArena(2); |
339 | tester.route(down2); |
340 | |
341 | tester.route(up2); |
342 | GestureBinding.instance.gestureArena.sweep(2); |
343 | |
344 | tester.route(up1); |
345 | GestureBinding.instance.gestureArena.sweep(1); |
346 | expect(events, <String>['down#1' , 'up#1' ]); |
347 | }); |
348 | |
349 | testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - after acceptance' , ( |
350 | GestureTester tester, |
351 | ) { |
352 | setUpTapAndPanGestureRecognizer(); |
353 | |
354 | tapAndDrag.addPointer(down1); |
355 | tester.closeArena(1); |
356 | tester.route(down1); |
357 | |
358 | tapAndDrag.addPointer(down2); |
359 | tester.closeArena(2); |
360 | tester.route(down2); |
361 | |
362 | tester.route(up1); |
363 | GestureBinding.instance.gestureArena.sweep(1); |
364 | |
365 | tester.route(up2); |
366 | GestureBinding.instance.gestureArena.sweep(2); |
367 | |
368 | expect(events, <String>['down#1' , 'up#1' ]); |
369 | }); |
370 | |
371 | testGesture('Recognizer rejects pointer that is not the primary one (FILO) - after acceptance' , ( |
372 | GestureTester tester, |
373 | ) { |
374 | setUpTapAndPanGestureRecognizer(); |
375 | |
376 | tapAndDrag.addPointer(down1); |
377 | tester.closeArena(1); |
378 | tester.route(down1); |
379 | |
380 | tapAndDrag.addPointer(down2); |
381 | tester.closeArena(2); |
382 | tester.route(down2); |
383 | |
384 | tester.route(up2); |
385 | GestureBinding.instance.gestureArena.sweep(2); |
386 | |
387 | tester.route(up1); |
388 | GestureBinding.instance.gestureArena.sweep(1); |
389 | expect(events, <String>['down#1' , 'up#1' ]); |
390 | }); |
391 | |
392 | testGesture('Recognizer detects tap gesture when pointer does not move past tap tolerance' , ( |
393 | GestureTester tester, |
394 | ) { |
395 | setUpTapAndPanGestureRecognizer(); |
396 | |
397 | // In this test the tap has not travelled past the tap tolerance defined by |
398 | // [kDoubleTapTouchSlop]. It is expected for the recognizer to detect a tap |
399 | // and fire drag cancel. |
400 | tapAndDrag.addPointer(down1); |
401 | tester.closeArena(1); |
402 | tester.route(down1); |
403 | tester.route(up1); |
404 | GestureBinding.instance.gestureArena.sweep(1); |
405 | expect(events, <String>['down#1' , 'up#1' ]); |
406 | }); |
407 | |
408 | testGesture( |
409 | 'Recognizer detects drag gesture when pointer moves past tap tolerance but not the drag minimum' , |
410 | (GestureTester tester) { |
411 | setUpTapAndPanGestureRecognizer(); |
412 | |
413 | // In this test, the pointer has moved past the tap tolerance but it has |
414 | // not reached the distance travelled to be considered a drag gesture. In |
415 | // this case it is expected for the recognizer to detect a drag and fire tap cancel. |
416 | tapAndDrag.addPointer(down5); |
417 | tester.closeArena(5); |
418 | tester.route(down5); |
419 | tester.route(move5); |
420 | tester.route(up5); |
421 | GestureBinding.instance.gestureArena.sweep(5); |
422 | expect(events, <String>['down#1' , 'panstart#1' , 'panend#1' ]); |
423 | }, |
424 | ); |
425 | |
426 | testGesture('Beats TapGestureRecognizer when mouse pointer moves past kPrecisePointerPanSlop' , ( |
427 | GestureTester tester, |
428 | ) { |
429 | setUpTapAndPanGestureRecognizer(); |
430 | |
431 | // This is a regression test for https://github.com/flutter/flutter/issues/122141. |
432 | final TapGestureRecognizer taps = |
433 | TapGestureRecognizer() |
434 | ..onTapDown = (TapDownDetails details) { |
435 | events.add('tapdown' ); |
436 | } |
437 | ..onTapUp = (TapUpDetails details) { |
438 | events.add('tapup' ); |
439 | } |
440 | ..onTapCancel = () { |
441 | events.add('tapscancel' ); |
442 | }; |
443 | addTearDown(taps.dispose); |
444 | |
445 | tapAndDrag.addPointer(down6); |
446 | taps.addPointer(down6); |
447 | tester.closeArena(6); |
448 | tester.route(down6); |
449 | tester.route(move6); |
450 | tester.route(up6); |
451 | GestureBinding.instance.gestureArena.sweep(6); |
452 | |
453 | expect(events, <String>['down#1' , 'panstart#1' , 'panupdate#1' , 'panend#1' ]); |
454 | }); |
455 | |
456 | testGesture( |
457 | 'Recognizer declares self-victory in a non-empty arena when pointer travels minimum distance to be considered a drag' , |
458 | (GestureTester tester) { |
459 | setUpTapAndPanGestureRecognizer(); |
460 | |
461 | final PanGestureRecognizer pans = |
462 | PanGestureRecognizer() |
463 | ..onStart = (DragStartDetails details) { |
464 | events.add('panstart' ); |
465 | } |
466 | ..onUpdate = (DragUpdateDetails details) { |
467 | events.add('panupdate' ); |
468 | } |
469 | ..onEnd = (DragEndDetails details) { |
470 | events.add('panend' ); |
471 | } |
472 | ..onCancel = () { |
473 | events.add('pancancel' ); |
474 | }; |
475 | addTearDown(pans.dispose); |
476 | |
477 | final TestPointer pointer = TestPointer(5); |
478 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
479 | // When competing against another [DragGestureRecognizer], the recognizer |
480 | // that first in the arena will win after sweep is called. |
481 | tapAndDrag.addPointer(downB); |
482 | pans.addPointer(downB); |
483 | tester.closeArena(5); |
484 | tester.route(downB); |
485 | tester.route(pointer.move(const Offset(40.0, 45.0))); |
486 | tester.route(pointer.up()); |
487 | expect(events, <String>['pancancel' , 'down#1' , 'panstart#1' , 'panupdate#1' , 'panend#1' ]); |
488 | }, |
489 | ); |
490 | |
491 | testGesture( |
492 | 'TapAndHorizontalDragGestureRecognizer accepts drag on a pan when the arena has already been won by the primary pointer' , |
493 | (GestureTester tester) { |
494 | setUpTapAndHorizontalDragGestureRecognizer(); |
495 | |
496 | final TestPointer pointer = TestPointer(5); |
497 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
498 | |
499 | tapAndDrag.addPointer(downB); |
500 | tester.closeArena(5); |
501 | tester.route(downB); |
502 | tester.route(pointer.move(const Offset(25.0, 45.0))); |
503 | tester.route(pointer.up()); |
504 | expect(events, <String>[ |
505 | 'down#1' , |
506 | 'horizontaldragstart#1' , |
507 | 'horizontaldragupdate#1' , |
508 | 'horizontaldragend#1' , |
509 | ]); |
510 | }, |
511 | ); |
512 | |
513 | testGesture( |
514 | 'TapAndHorizontalDragGestureRecognizer loses to VerticalDragGestureRecognizer on a vertical drag' , |
515 | (GestureTester tester) { |
516 | setUpTapAndHorizontalDragGestureRecognizer(); |
517 | |
518 | final VerticalDragGestureRecognizer verticalDrag = |
519 | VerticalDragGestureRecognizer() |
520 | ..onStart = (DragStartDetails details) { |
521 | events.add('verticalstart' ); |
522 | } |
523 | ..onUpdate = (DragUpdateDetails details) { |
524 | events.add('verticalupdate' ); |
525 | } |
526 | ..onEnd = (DragEndDetails details) { |
527 | events.add('verticalend' ); |
528 | } |
529 | ..onCancel = () { |
530 | events.add('verticalcancel' ); |
531 | }; |
532 | addTearDown(verticalDrag.dispose); |
533 | |
534 | final TestPointer pointer = TestPointer(5); |
535 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
536 | |
537 | tapAndDrag.addPointer(downB); |
538 | verticalDrag.addPointer(downB); |
539 | tester.closeArena(5); |
540 | tester.route(downB); |
541 | tester.route(pointer.move(const Offset(10.0, 45.0))); |
542 | tester.route(pointer.move(const Offset(10.0, 100.0))); |
543 | tester.route(pointer.up()); |
544 | expect(events, <String>['verticalstart' , 'verticalupdate' , 'verticalend' ]); |
545 | }, |
546 | ); |
547 | |
548 | testGesture( |
549 | 'TapAndPanGestureRecognizer loses to VerticalDragGestureRecognizer on a vertical drag' , |
550 | (GestureTester tester) { |
551 | setUpTapAndPanGestureRecognizer(); |
552 | |
553 | final VerticalDragGestureRecognizer verticalDrag = |
554 | VerticalDragGestureRecognizer() |
555 | ..onStart = (DragStartDetails details) { |
556 | events.add('verticalstart' ); |
557 | } |
558 | ..onUpdate = (DragUpdateDetails details) { |
559 | events.add('verticalupdate' ); |
560 | } |
561 | ..onEnd = (DragEndDetails details) { |
562 | events.add('verticalend' ); |
563 | } |
564 | ..onCancel = () { |
565 | events.add('verticalcancel' ); |
566 | }; |
567 | addTearDown(verticalDrag.dispose); |
568 | |
569 | final TestPointer pointer = TestPointer(5); |
570 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
571 | |
572 | tapAndDrag.addPointer(downB); |
573 | verticalDrag.addPointer(downB); |
574 | tester.closeArena(5); |
575 | tester.route(downB); |
576 | tester.route(pointer.move(const Offset(10.0, 45.0))); |
577 | tester.route(pointer.move(const Offset(10.0, 100.0))); |
578 | tester.route(pointer.up()); |
579 | expect(events, <String>['verticalstart' , 'verticalupdate' , 'verticalend' ]); |
580 | }, |
581 | ); |
582 | |
583 | testGesture( |
584 | 'TapAndHorizontalDragGestureRecognizer beats VerticalDragGestureRecognizer on a horizontal drag' , |
585 | (GestureTester tester) { |
586 | setUpTapAndHorizontalDragGestureRecognizer(); |
587 | |
588 | final VerticalDragGestureRecognizer verticalDrag = |
589 | VerticalDragGestureRecognizer() |
590 | ..onStart = (DragStartDetails details) { |
591 | events.add('verticalstart' ); |
592 | } |
593 | ..onUpdate = (DragUpdateDetails details) { |
594 | events.add('verticalupdate' ); |
595 | } |
596 | ..onEnd = (DragEndDetails details) { |
597 | events.add('verticalend' ); |
598 | } |
599 | ..onCancel = () { |
600 | events.add('verticalcancel' ); |
601 | }; |
602 | addTearDown(verticalDrag.dispose); |
603 | |
604 | final TestPointer pointer = TestPointer(5); |
605 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
606 | |
607 | tapAndDrag.addPointer(downB); |
608 | verticalDrag.addPointer(downB); |
609 | tester.closeArena(5); |
610 | tester.route(downB); |
611 | tester.route(pointer.move(const Offset(45.0, 10.0))); |
612 | tester.route(pointer.up()); |
613 | expect(events, <String>[ |
614 | 'verticalcancel' , |
615 | 'down#1' , |
616 | 'horizontaldragstart#1' , |
617 | 'horizontaldragupdate#1' , |
618 | 'horizontaldragend#1' , |
619 | ]); |
620 | }, |
621 | ); |
622 | |
623 | testGesture( |
624 | 'TapAndPanGestureRecognizer beats VerticalDragGestureRecognizer on a horizontal pan' , |
625 | (GestureTester tester) { |
626 | setUpTapAndPanGestureRecognizer(); |
627 | |
628 | final VerticalDragGestureRecognizer verticalDrag = |
629 | VerticalDragGestureRecognizer() |
630 | ..onStart = (DragStartDetails details) { |
631 | events.add('verticalstart' ); |
632 | } |
633 | ..onUpdate = (DragUpdateDetails details) { |
634 | events.add('verticalupdate' ); |
635 | } |
636 | ..onEnd = (DragEndDetails details) { |
637 | events.add('verticalend' ); |
638 | } |
639 | ..onCancel = () { |
640 | events.add('verticalcancel' ); |
641 | }; |
642 | addTearDown(verticalDrag.dispose); |
643 | |
644 | final TestPointer pointer = TestPointer(5); |
645 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
646 | |
647 | tapAndDrag.addPointer(downB); |
648 | verticalDrag.addPointer(downB); |
649 | tester.closeArena(5); |
650 | tester.route(downB); |
651 | tester.route(pointer.move(const Offset(45.0, 25.0))); |
652 | tester.route(pointer.up()); |
653 | expect(events, <String>['verticalcancel' , 'down#1' , 'panstart#1' , 'panupdate#1' , 'panend#1' ]); |
654 | }, |
655 | ); |
656 | |
657 | testGesture( |
658 | 'Recognizer loses when competing against a DragGestureRecognizer for a drag when eagerVictoryOnDrag is disabled' , |
659 | (GestureTester tester) { |
660 | setUpTapAndPanGestureRecognizer(eagerVictoryOnDrag: false); |
661 | final PanGestureRecognizer pans = |
662 | PanGestureRecognizer() |
663 | ..onStart = (DragStartDetails details) { |
664 | events.add('panstart' ); |
665 | } |
666 | ..onUpdate = (DragUpdateDetails details) { |
667 | events.add('panupdate' ); |
668 | } |
669 | ..onEnd = (DragEndDetails details) { |
670 | events.add('panend' ); |
671 | } |
672 | ..onCancel = () { |
673 | events.add('pancancel' ); |
674 | }; |
675 | addTearDown(pans.dispose); |
676 | |
677 | final TestPointer pointer = TestPointer(5); |
678 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
679 | // When competing against another [DragGestureRecognizer], the [TapAndPanGestureRecognizer] |
680 | // will only win when it is the last recognizer in the arena. |
681 | tapAndDrag.addPointer(downB); |
682 | pans.addPointer(downB); |
683 | tester.closeArena(5); |
684 | tester.route(downB); |
685 | tester.route(pointer.move(const Offset(40.0, 45.0))); |
686 | tester.route(pointer.up()); |
687 | expect(events, <String>['panstart' , 'panend' ]); |
688 | }, |
689 | ); |
690 | |
691 | testGesture('Drag state is properly reset after losing GestureArena' , (GestureTester tester) { |
692 | setUpTapAndHorizontalDragGestureRecognizer(eagerVictoryOnDrag: false); |
693 | final HorizontalDragGestureRecognizer horizontalDrag = |
694 | HorizontalDragGestureRecognizer() |
695 | ..onStart = (DragStartDetails details) { |
696 | events.add('basichorizontalstart' ); |
697 | } |
698 | ..onUpdate = (DragUpdateDetails details) { |
699 | events.add('basichorizontalupdate' ); |
700 | } |
701 | ..onEnd = (DragEndDetails details) { |
702 | events.add('basichorizontalend' ); |
703 | } |
704 | ..onCancel = () { |
705 | events.add('basichorizontalcancel' ); |
706 | }; |
707 | addTearDown(horizontalDrag.dispose); |
708 | |
709 | final LongPressGestureRecognizer longpress = |
710 | LongPressGestureRecognizer() |
711 | ..onLongPressStart = (LongPressStartDetails details) { |
712 | events.add('longpressstart' ); |
713 | } |
714 | ..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) { |
715 | events.add('longpressmoveupdate' ); |
716 | } |
717 | ..onLongPressEnd = (LongPressEndDetails details) { |
718 | events.add('longpressend' ); |
719 | } |
720 | ..onLongPressCancel = () { |
721 | events.add('longpresscancel' ); |
722 | }; |
723 | addTearDown(longpress.dispose); |
724 | |
725 | FlutterErrorDetails? errorDetails; |
726 | final FlutterExceptionHandler? oldHandler = FlutterError.onError; |
727 | FlutterError.onError = (FlutterErrorDetails details) { |
728 | errorDetails = details; |
729 | }; |
730 | |
731 | final TestPointer pointer = TestPointer(5); |
732 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
733 | // When competing against another [DragGestureRecognizer], the [TapAndPanGestureRecognizer] |
734 | // will only win when it is the last recognizer in the arena. |
735 | tapAndDrag.addPointer(downB); |
736 | horizontalDrag.addPointer(downB); |
737 | longpress.addPointer(downB); |
738 | tester.closeArena(5); |
739 | tester.route(downB); |
740 | tester.route(pointer.move(const Offset(28.1, 10.0))); |
741 | tester.route(pointer.up()); |
742 | expect(events, <String>['basichorizontalstart' , 'basichorizontalend' ]); |
743 | |
744 | final PointerDownEvent downC = pointer.down(const Offset(10.0, 10.0)); |
745 | tapAndDrag.addPointer(downC); |
746 | horizontalDrag.addPointer(downC); |
747 | longpress.addPointer(downC); |
748 | tester.closeArena(5); |
749 | tester.route(downC); |
750 | tester.route(pointer.up()); |
751 | FlutterError.onError = oldHandler; |
752 | expect(errorDetails, isNull); |
753 | }); |
754 | |
755 | testGesture('Beats LongPressGestureRecognizer on a consecutive tap greater than one' , ( |
756 | GestureTester tester, |
757 | ) { |
758 | setUpTapAndPanGestureRecognizer(); |
759 | |
760 | final LongPressGestureRecognizer longpress = |
761 | LongPressGestureRecognizer() |
762 | ..onLongPressStart = (LongPressStartDetails details) { |
763 | events.add('longpressstart' ); |
764 | } |
765 | ..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) { |
766 | events.add('longpressmoveupdate' ); |
767 | } |
768 | ..onLongPressEnd = (LongPressEndDetails details) { |
769 | events.add('longpressend' ); |
770 | } |
771 | ..onLongPressCancel = () { |
772 | events.add('longpresscancel' ); |
773 | }; |
774 | addTearDown(longpress.dispose); |
775 | |
776 | final TestPointer pointer = TestPointer(5); |
777 | final PointerDownEvent downA = pointer.down(const Offset(10.0, 10.0)); |
778 | tapAndDrag.addPointer(downA); |
779 | longpress.addPointer(downA); |
780 | tester.closeArena(5); |
781 | tester.route(downA); |
782 | tester.route(pointer.up()); |
783 | GestureBinding.instance.gestureArena.sweep(5); |
784 | |
785 | tester.async.elapse(kConsecutiveTapDelay); |
786 | |
787 | final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); |
788 | tapAndDrag.addPointer(downB); |
789 | longpress.addPointer(downB); |
790 | tester.closeArena(5); |
791 | tester.route(downB); |
792 | |
793 | tester.async.elapse(const Duration(milliseconds: 500)); |
794 | |
795 | tester.route(pointer.move(const Offset(40.0, 45.0))); |
796 | tester.route(pointer.up()); |
797 | expect(events, <String>[ |
798 | 'longpresscancel' , |
799 | 'down#1' , |
800 | 'up#1' , |
801 | 'down#2' , |
802 | 'panstart#2' , |
803 | 'panupdate#2' , |
804 | 'panend#2' , |
805 | ]); |
806 | }); |
807 | |
808 | // This is a regression test for https://github.com/flutter/flutter/issues/129161. |
809 | testGesture( |
810 | 'Beats TapGestureRecognizer and DoubleTapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena' , |
811 | (GestureTester tester) { |
812 | setUpTapAndPanGestureRecognizer(); |
813 | |
814 | final TapGestureRecognizer taps = |
815 | TapGestureRecognizer() |
816 | ..onTapDown = (TapDownDetails details) { |
817 | events.add('tapdown' ); |
818 | } |
819 | ..onTapUp = (TapUpDetails details) { |
820 | events.add('tapup' ); |
821 | } |
822 | ..onTapCancel = () { |
823 | events.add('tapscancel' ); |
824 | }; |
825 | addTearDown(taps.dispose); |
826 | |
827 | final DoubleTapGestureRecognizer doubleTaps = |
828 | DoubleTapGestureRecognizer() |
829 | ..onDoubleTapDown = (TapDownDetails details) { |
830 | events.add('doubletapdown' ); |
831 | } |
832 | ..onDoubleTap = () { |
833 | events.add('doubletapup' ); |
834 | } |
835 | ..onDoubleTapCancel = () { |
836 | events.add('doubletapcancel' ); |
837 | }; |
838 | addTearDown(doubleTaps.dispose); |
839 | |
840 | tapAndDrag.addPointer(down1); |
841 | taps.addPointer(down1); |
842 | doubleTaps.addPointer(down1); |
843 | tester.closeArena(1); |
844 | tester.route(down1); |
845 | tester.route(up1); |
846 | GestureBinding.instance.gestureArena.sweep(1); |
847 | // Wait for GestureArena to resolve itself. |
848 | tester.async.elapse(kDoubleTapTimeout); |
849 | expect(events, <String>['down#1' , 'up#1' ]); |
850 | }, |
851 | ); |
852 | |
853 | testGesture( |
854 | 'Beats TapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena' , |
855 | (GestureTester tester) { |
856 | setUpTapAndPanGestureRecognizer(); |
857 | |
858 | final TapGestureRecognizer taps = |
859 | TapGestureRecognizer() |
860 | ..onTapDown = (TapDownDetails details) { |
861 | events.add('tapdown' ); |
862 | } |
863 | ..onTapUp = (TapUpDetails details) { |
864 | events.add('tapup' ); |
865 | } |
866 | ..onTapCancel = () { |
867 | events.add('tapscancel' ); |
868 | }; |
869 | addTearDown(taps.dispose); |
870 | tapAndDrag.addPointer(down1); |
871 | taps.addPointer(down1); |
872 | tester.closeArena(1); |
873 | tester.route(down1); |
874 | tester.route(up1); |
875 | GestureBinding.instance.gestureArena.sweep(1); |
876 | expect(events, <String>['down#1' , 'up#1' ]); |
877 | }, |
878 | ); |
879 | |
880 | testGesture('Beats TapGestureRecognizer when the pointer has exceeded the slop tolerance' , ( |
881 | GestureTester tester, |
882 | ) { |
883 | setUpTapAndPanGestureRecognizer(); |
884 | |
885 | final TapGestureRecognizer taps = |
886 | TapGestureRecognizer() |
887 | ..onTapDown = (TapDownDetails details) { |
888 | events.add('tapdown' ); |
889 | } |
890 | ..onTapUp = (TapUpDetails details) { |
891 | events.add('tapup' ); |
892 | } |
893 | ..onTapCancel = () { |
894 | events.add('tapscancel' ); |
895 | }; |
896 | addTearDown(taps.dispose); |
897 | |
898 | tapAndDrag.addPointer(down5); |
899 | taps.addPointer(down5); |
900 | tester.closeArena(5); |
901 | tester.route(down5); |
902 | tester.route(move5); |
903 | tester.route(up5); |
904 | GestureBinding.instance.gestureArena.sweep(5); |
905 | expect(events, <String>['down#1' , 'panstart#1' , 'panend#1' ]); |
906 | |
907 | events.clear(); |
908 | tester.async.elapse(const Duration(milliseconds: 1000)); |
909 | taps.addPointer(down1); |
910 | tapAndDrag.addPointer(down1); |
911 | tester.closeArena(1); |
912 | tester.route(down1); |
913 | tester.route(up1); |
914 | GestureBinding.instance.gestureArena.sweep(1); |
915 | expect(events, <String>['tapdown' , 'tapup' ]); |
916 | }); |
917 | |
918 | testGesture( |
919 | 'Ties with PanGestureRecognizer when pointer has not met sufficient global distance to be a drag' , |
920 | (GestureTester tester) { |
921 | setUpTapAndPanGestureRecognizer(); |
922 | |
923 | final PanGestureRecognizer pans = |
924 | PanGestureRecognizer() |
925 | ..onStart = (DragStartDetails details) { |
926 | events.add('panstart' ); |
927 | } |
928 | ..onUpdate = (DragUpdateDetails details) { |
929 | events.add('panupdate' ); |
930 | } |
931 | ..onEnd = (DragEndDetails details) { |
932 | events.add('panend' ); |
933 | } |
934 | ..onCancel = () { |
935 | events.add('pancancel' ); |
936 | }; |
937 | addTearDown(pans.dispose); |
938 | |
939 | tapAndDrag.addPointer(down5); |
940 | pans.addPointer(down5); |
941 | tester.closeArena(5); |
942 | tester.route(down5); |
943 | tester.route(move5); |
944 | tester.route(up5); |
945 | GestureBinding.instance.gestureArena.sweep(5); |
946 | expect(events, <String>['pancancel' ]); |
947 | }, |
948 | ); |
949 | |
950 | testGesture('Defaults to drag when pointer dragged past slop tolerance' , (GestureTester tester) { |
951 | setUpTapAndPanGestureRecognizer(); |
952 | |
953 | tapAndDrag.addPointer(down5); |
954 | tester.closeArena(5); |
955 | tester.route(down5); |
956 | tester.route(move5); |
957 | tester.route(up5); |
958 | GestureBinding.instance.gestureArena.sweep(5); |
959 | expect(events, <String>['down#1' , 'panstart#1' , 'panend#1' ]); |
960 | |
961 | events.clear(); |
962 | tester.async.elapse(const Duration(milliseconds: 1000)); |
963 | tapAndDrag.addPointer(down1); |
964 | tester.closeArena(1); |
965 | tester.route(down1); |
966 | tester.route(up1); |
967 | GestureBinding.instance.gestureArena.sweep(1); |
968 | expect(events, <String>['down#1' , 'up#1' ]); |
969 | }); |
970 | |
971 | testGesture('Fires cancel and resets for PointerCancelEvent' , (GestureTester tester) { |
972 | setUpTapAndPanGestureRecognizer(); |
973 | |
974 | tapAndDrag.addPointer(down1); |
975 | tester.closeArena(1); |
976 | tester.route(down1); |
977 | tester.route(cancel1); |
978 | GestureBinding.instance.gestureArena.sweep(1); |
979 | expect(events, <String>['down#1' , 'cancel' ]); |
980 | |
981 | events.clear(); |
982 | tester.async.elapse(const Duration(milliseconds: 100)); |
983 | tapAndDrag.addPointer(down2); |
984 | tester.closeArena(2); |
985 | tester.route(down2); |
986 | tester.route(up2); |
987 | GestureBinding.instance.gestureArena.sweep(2); |
988 | expect(events, <String>['down#1' , 'up#1' ]); |
989 | }); |
990 | |
991 | // This is a regression test for https://github.com/flutter/flutter/issues/102084. |
992 | testGesture('Does not call onDragEnd if not provided' , (GestureTester tester) { |
993 | tapAndDrag = |
994 | TapAndDragGestureRecognizer() |
995 | ..dragStartBehavior = DragStartBehavior.down |
996 | ..maxConsecutiveTap = 3 |
997 | ..onTapDown = (TapDragDownDetails details) { |
998 | events.add('down# ${details.consecutiveTapCount}' ); |
999 | }; |
1000 | addTearDown(tapAndDrag.dispose); |
1001 | |
1002 | FlutterErrorDetails? errorDetails; |
1003 | final FlutterExceptionHandler? oldHandler = FlutterError.onError; |
1004 | FlutterError.onError = (FlutterErrorDetails details) { |
1005 | errorDetails = details; |
1006 | }; |
1007 | addTearDown(() { |
1008 | FlutterError.onError = oldHandler; |
1009 | }); |
1010 | |
1011 | tapAndDrag.addPointer(down5); |
1012 | tester.closeArena(5); |
1013 | tester.route(down5); |
1014 | tester.route(move5); |
1015 | tester.route(up5); |
1016 | GestureBinding.instance.gestureArena.sweep(5); |
1017 | expect(events, <String>['down#1' ]); |
1018 | |
1019 | expect(errorDetails, isNull); |
1020 | |
1021 | events.clear(); |
1022 | tester.async.elapse(const Duration(milliseconds: 1000)); |
1023 | tapAndDrag.addPointer(down1); |
1024 | tester.closeArena(1); |
1025 | tester.route(down1); |
1026 | tester.route(up1); |
1027 | GestureBinding.instance.gestureArena.sweep(1); |
1028 | expect(events, <String>['down#1' ]); |
1029 | }); |
1030 | |
1031 | testGesture('Contains correct positions in the drag end details' , (GestureTester tester) { |
1032 | late TapDragEndDetails tapDragEndDetails; |
1033 | tapAndDrag = |
1034 | TapAndHorizontalDragGestureRecognizer() |
1035 | ..dragStartBehavior = DragStartBehavior.down |
1036 | ..eagerVictoryOnDrag = true |
1037 | ..maxConsecutiveTap = 3 |
1038 | ..onDragEnd = (TapDragEndDetails details) { |
1039 | tapDragEndDetails = details; |
1040 | }; |
1041 | addTearDown(tapAndDrag.dispose); |
1042 | |
1043 | final TestPointer pointer = TestPointer(5); |
1044 | final PointerDownEvent pointerDown = pointer.down(const Offset(10.0, 10.0)); |
1045 | |
1046 | tapAndDrag.addPointer(pointerDown); |
1047 | tester.closeArena(pointer.pointer); |
1048 | tester.route(pointerDown); |
1049 | tester.route(pointer.move(const Offset(50.0, 20.0))); |
1050 | tester.route(pointer.move(const Offset(90.0, 30.0))); |
1051 | tester.route(pointer.move(const Offset(120.0, 45.0))); |
1052 | tester.route(pointer.up()); |
1053 | |
1054 | expect(tapDragEndDetails.globalPosition, const Offset(120.0, 45.0)); |
1055 | }); |
1056 | } |
1057 | |