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
5import 'package:flutter/foundation.dart';
6import 'package:flutter/gestures.dart';
7import 'package:flutter_test/flutter_test.dart';
8
9import '../gestures/gesture_tester.dart';
10
11// Anything longer than [kDoubleTapTimeout] will reset the consecutive tap count.
12final Duration kConsecutiveTapDelay = kDoubleTapTimeout ~/ 2;
13
14void 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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com