2
2
// Use of this source code is governed by a BSD-style license that can be
3
3
// found in the LICENSE file.
4
4
5
+ import 'dart:math' ;
6
+
5
7
import 'package:flutter/foundation.dart' ;
8
+ import 'package:flutter/scheduler.dart' ;
6
9
7
10
import 'constants.dart' ;
8
11
import 'drag_details.dart' ;
@@ -119,6 +122,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
119
122
/// will only track the latest active (accepted by this recognizer) pointer, which
120
123
/// appears to be only one finger dragging.
121
124
///
125
+ /// If set to [MultitouchDragStrategy.averageBoundaryPointers] , all active
126
+ /// pointers will be tracked, and the result is computed from the boundary pointers.
127
+ ///
122
128
/// If set to [MultitouchDragStrategy.sumAllPointers] ,
123
129
/// all active pointers will be tracked together and the scrolling offset
124
130
/// is the sum of the offsets of all active pointers
@@ -128,7 +134,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
128
134
///
129
135
/// See also:
130
136
///
131
- /// * [MultitouchDragStrategy] , which defines two different drag strategies for
137
+ /// * [MultitouchDragStrategy] , which defines several different drag strategies for
132
138
/// multi-finger drag.
133
139
MultitouchDragStrategy multitouchDragStrategy;
134
140
@@ -323,11 +329,27 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
323
329
324
330
Offset _getDeltaForDetails (Offset delta);
325
331
double ? _getPrimaryValueFromOffset (Offset value);
332
+
333
+ /// The axis (horizontal or vertical) corresponding to the primary drag direction.
334
+ ///
335
+ /// The [PanGestureRecognizer] returns null.
336
+ _DragDirection ? _getPrimaryDragAxis () => null ;
326
337
bool _hasSufficientGlobalDistanceToAccept (PointerDeviceKind pointerDeviceKind, double ? deviceTouchSlop);
327
338
bool _hasDragThresholdBeenMet = false ;
328
339
329
340
final Map <int , VelocityTracker > _velocityTrackers = < int , VelocityTracker > {};
330
341
342
+ // The move delta of each pointer before the next frame.
343
+ //
344
+ // The key is the pointer ID. It is cleared whenever a new batch of pointer events is detected.
345
+ final Map <int , Offset > _moveDeltaBeforeFrame = < int , Offset > {};
346
+
347
+ // The timestamp of all events of the current frame.
348
+ //
349
+ // On a event with a different timestamp, the event is considered a new batch.
350
+ Duration ? _frameTimeStamp;
351
+ Offset _lastUpdatedDeltaForPan = Offset .zero;
352
+
331
353
@override
332
354
bool isPointerAllowed (PointerEvent event) {
333
355
if (_initialButtons == null ) {
@@ -389,13 +411,194 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
389
411
final bool result;
390
412
switch (multitouchDragStrategy) {
391
413
case MultitouchDragStrategy .sumAllPointers:
414
+ case MultitouchDragStrategy .averageBoundaryPointers:
392
415
result = true ;
393
416
case MultitouchDragStrategy .latestPointer:
394
- result = _acceptedActivePointers.length <= 1 || pointer == _acceptedActivePointers.last ;
417
+ result = _activePointer == null || pointer == _activePointer ;
395
418
}
396
419
return result;
397
420
}
398
421
422
+ void _recordMoveDeltaForMultitouch (int pointer, Offset localDelta) {
423
+ if (multitouchDragStrategy != MultitouchDragStrategy .averageBoundaryPointers) {
424
+ assert (_frameTimeStamp == null );
425
+ assert (_moveDeltaBeforeFrame.isEmpty);
426
+ return ;
427
+ }
428
+
429
+ assert (_frameTimeStamp == SchedulerBinding .instance.currentSystemFrameTimeStamp);
430
+
431
+ if (_state != _DragState .accepted || localDelta == Offset .zero) {
432
+ return ;
433
+ }
434
+
435
+ if (_moveDeltaBeforeFrame.containsKey (pointer)) {
436
+ final Offset offset = _moveDeltaBeforeFrame[pointer]! ;
437
+ _moveDeltaBeforeFrame[pointer] = offset + localDelta;
438
+ } else {
439
+ _moveDeltaBeforeFrame[pointer] = localDelta;
440
+ }
441
+ }
442
+
443
+ double _getSumDelta ({
444
+ required int pointer,
445
+ required bool positive,
446
+ required _DragDirection axis,
447
+ }) {
448
+ double sum = 0.0 ;
449
+
450
+ if (! _moveDeltaBeforeFrame.containsKey (pointer)) {
451
+ return sum;
452
+ }
453
+
454
+ final Offset offset = _moveDeltaBeforeFrame[pointer]! ;
455
+ if (positive) {
456
+ if (axis == _DragDirection .vertical) {
457
+ sum = max (offset.dy, 0.0 );
458
+ } else {
459
+ sum = max (offset.dx, 0.0 );
460
+ }
461
+ } else {
462
+ if (axis == _DragDirection .vertical) {
463
+ sum = min (offset.dy, 0.0 );
464
+ } else {
465
+ sum = min (offset.dx, 0.0 );
466
+ }
467
+ }
468
+
469
+ return sum;
470
+ }
471
+
472
+ int ? _getMaxSumDeltaPointer ({
473
+ required bool positive,
474
+ required _DragDirection axis,
475
+ }) {
476
+ if (_moveDeltaBeforeFrame.isEmpty) {
477
+ return null ;
478
+ }
479
+
480
+ int ? ret;
481
+ double ? max;
482
+ double sum;
483
+ for (final int pointer in _moveDeltaBeforeFrame.keys) {
484
+ sum = _getSumDelta (pointer: pointer, positive: positive, axis: axis);
485
+ if (ret == null ) {
486
+ ret = pointer;
487
+ max = sum;
488
+ } else {
489
+ if (positive) {
490
+ if (sum > max! ) {
491
+ ret = pointer;
492
+ max = sum;
493
+ }
494
+ } else {
495
+ if (sum < max! ) {
496
+ ret = pointer;
497
+ max = sum;
498
+ }
499
+ }
500
+ }
501
+ }
502
+ assert (ret != null );
503
+ return ret;
504
+ }
505
+
506
+ Offset _resolveLocalDeltaForMultitouch (int pointer, Offset localDelta) {
507
+ if (multitouchDragStrategy != MultitouchDragStrategy .averageBoundaryPointers) {
508
+ if (_frameTimeStamp != null ) {
509
+ _moveDeltaBeforeFrame.clear ();
510
+ _frameTimeStamp = null ;
511
+ _lastUpdatedDeltaForPan = Offset .zero;
512
+ }
513
+ return localDelta;
514
+ }
515
+
516
+ final Duration currentSystemFrameTimeStamp = SchedulerBinding .instance.currentSystemFrameTimeStamp;
517
+ if (_frameTimeStamp != currentSystemFrameTimeStamp) {
518
+ _moveDeltaBeforeFrame.clear ();
519
+ _lastUpdatedDeltaForPan = Offset .zero;
520
+ _frameTimeStamp = currentSystemFrameTimeStamp;
521
+ }
522
+
523
+ assert (_frameTimeStamp == SchedulerBinding .instance.currentSystemFrameTimeStamp);
524
+
525
+ final _DragDirection ? axis = _getPrimaryDragAxis ();
526
+
527
+ if (_state != _DragState .accepted || localDelta == Offset .zero || (_moveDeltaBeforeFrame.isEmpty && axis != null )) {
528
+ return localDelta;
529
+ }
530
+
531
+ final double dx,dy;
532
+ if (axis == _DragDirection .horizontal) {
533
+ dx = _resolveDelta (pointer: pointer, axis: _DragDirection .horizontal, localDelta: localDelta);
534
+ assert (dx.abs () <= localDelta.dx.abs ());
535
+ dy = 0.0 ;
536
+ } else if (axis == _DragDirection .vertical) {
537
+ dx = 0.0 ;
538
+ dy = _resolveDelta (pointer: pointer, axis: _DragDirection .vertical, localDelta: localDelta);
539
+ assert (dy.abs () <= localDelta.dy.abs ());
540
+ } else {
541
+ final double averageX = _resolveDeltaForPanGesture (axis: _DragDirection .horizontal, localDelta: localDelta);
542
+ final double averageY = _resolveDeltaForPanGesture (axis: _DragDirection .vertical, localDelta: localDelta);
543
+ final Offset updatedDelta = Offset (averageX, averageY) - _lastUpdatedDeltaForPan;
544
+ _lastUpdatedDeltaForPan = Offset (averageX, averageY);
545
+ dx = updatedDelta.dx;
546
+ dy = updatedDelta.dy;
547
+ }
548
+
549
+ return Offset (dx, dy);
550
+ }
551
+
552
+ double _resolveDelta ({
553
+ required int pointer,
554
+ required _DragDirection axis,
555
+ required Offset localDelta,
556
+ }) {
557
+ final bool positive = axis == _DragDirection .horizontal ? localDelta.dx > 0 : localDelta.dy > 0 ;
558
+ final double delta = axis == _DragDirection .horizontal ? localDelta.dx : localDelta.dy;
559
+ final int ? maxSumDeltaPointer = _getMaxSumDeltaPointer (positive: positive, axis: axis);
560
+ assert (maxSumDeltaPointer != null );
561
+
562
+ if (maxSumDeltaPointer == pointer) {
563
+ return delta;
564
+ } else {
565
+ final double maxSumDelta = _getSumDelta (pointer: maxSumDeltaPointer! , positive: positive, axis: axis);
566
+ final double curPointerSumDelta = _getSumDelta (pointer: pointer, positive: positive, axis: axis);
567
+ if (positive) {
568
+ if (curPointerSumDelta + delta > maxSumDelta) {
569
+ return curPointerSumDelta + delta - maxSumDelta;
570
+ } else {
571
+ return 0.0 ;
572
+ }
573
+ } else {
574
+ if (curPointerSumDelta + delta < maxSumDelta) {
575
+ return curPointerSumDelta + delta - maxSumDelta;
576
+ } else {
577
+ return 0.0 ;
578
+ }
579
+ }
580
+ }
581
+ }
582
+
583
+ double _resolveDeltaForPanGesture ({
584
+ required _DragDirection axis,
585
+ required Offset localDelta,
586
+ }) {
587
+ final double delta = axis == _DragDirection .horizontal ? localDelta.dx : localDelta.dy;
588
+ final int pointerCount = _acceptedActivePointers.length;
589
+ assert (pointerCount >= 1 );
590
+
591
+ double sum = delta;
592
+ for (final Offset offset in _moveDeltaBeforeFrame.values) {
593
+ if (axis == _DragDirection .horizontal) {
594
+ sum += offset.dx;
595
+ } else {
596
+ sum += offset.dy;
597
+ }
598
+ }
599
+ return sum / pointerCount;
600
+ }
601
+
399
602
@override
400
603
void handleEvent (PointerEvent event) {
401
604
assert (_state != _DragState .ready);
@@ -424,6 +627,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
424
627
final Offset position = (event is PointerMoveEvent ) ? event.position : (event.position + (event as PointerPanZoomUpdateEvent ).pan);
425
628
final Offset localPosition = (event is PointerMoveEvent ) ? event.localPosition : (event.localPosition + (event as PointerPanZoomUpdateEvent ).localPan);
426
629
_finalPosition = OffsetPair (local: localPosition, global: position);
630
+ final Offset resolvedDelta = _resolveLocalDeltaForMultitouch (event.pointer, localDelta);
427
631
switch (_state) {
428
632
case _DragState .ready || _DragState .possible:
429
633
_pendingDragOffset += OffsetPair (local: localDelta, global: delta);
@@ -447,24 +651,32 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
447
651
case _DragState .accepted:
448
652
_checkUpdate (
449
653
sourceTimeStamp: event.timeStamp,
450
- delta: _getDeltaForDetails (localDelta ),
451
- primaryDelta: _getPrimaryValueFromOffset (localDelta ),
654
+ delta: _getDeltaForDetails (resolvedDelta ),
655
+ primaryDelta: _getPrimaryValueFromOffset (resolvedDelta ),
452
656
globalPosition: position,
453
657
localPosition: localPosition,
454
658
);
455
659
}
660
+ _recordMoveDeltaForMultitouch (event.pointer, localDelta);
456
661
}
457
662
if (event case PointerUpEvent () || PointerCancelEvent () || PointerPanZoomEndEvent ()) {
458
663
_giveUpPointer (event.pointer);
459
664
}
460
665
}
461
666
462
667
final List <int > _acceptedActivePointers = < int > [];
668
+ // This value is used when the multitouch strategy is `latestPointer`,
669
+ // it keeps track of the last accepted pointer. If this active pointer
670
+ // leave up, it will be set to the first accepted pointer.
671
+ // Refer to the implementation of Android `RecyclerView`(line 3846):
672
+ // https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
673
+ int ? _activePointer;
463
674
464
675
@override
465
676
void acceptGesture (int pointer) {
466
677
assert (! _acceptedActivePointers.contains (pointer));
467
678
_acceptedActivePointers.add (pointer);
679
+ _activePointer = pointer;
468
680
if (! onlyAcceptDragOnThreshold || _hasDragThresholdBeenMet) {
469
681
_checkDrag (pointer);
470
682
}
@@ -502,6 +714,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
502
714
if (! _acceptedActivePointers.remove (pointer)) {
503
715
resolvePointer (pointer, GestureDisposition .rejected);
504
716
}
717
+
718
+ _moveDeltaBeforeFrame.remove (pointer);
719
+ if (_activePointer == pointer) {
720
+ _activePointer =
721
+ _acceptedActivePointers.isNotEmpty ? _acceptedActivePointers.first : null ;
722
+ }
505
723
}
506
724
507
725
void _checkDown () {
@@ -687,6 +905,9 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
687
905
@override
688
906
double _getPrimaryValueFromOffset (Offset value) => value.dy;
689
907
908
+ @override
909
+ _DragDirection ? _getPrimaryDragAxis () => _DragDirection .vertical;
910
+
690
911
@override
691
912
String get debugDescription => 'vertical drag' ;
692
913
}
@@ -744,6 +965,9 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
744
965
@override
745
966
double _getPrimaryValueFromOffset (Offset value) => value.dx;
746
967
968
+ @override
969
+ _DragDirection ? _getPrimaryDragAxis () => _DragDirection .horizontal;
970
+
747
971
@override
748
972
String get debugDescription => 'horizontal drag' ;
749
973
}
@@ -801,3 +1025,8 @@ class PanGestureRecognizer extends DragGestureRecognizer {
801
1025
@override
802
1026
String get debugDescription => 'pan' ;
803
1027
}
1028
+
1029
+ enum _DragDirection {
1030
+ horizontal,
1031
+ vertical,
1032
+ }
0 commit comments