22// Use of this source code is governed by a BSD-style license that can be
33// found in the LICENSE file.
44
5+ import 'dart:math' ;
6+
57import 'package:flutter/foundation.dart' ;
8+ import 'package:flutter/scheduler.dart' ;
69
710import 'constants.dart' ;
811import 'drag_details.dart' ;
@@ -119,6 +122,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
119122 /// will only track the latest active (accepted by this recognizer) pointer, which
120123 /// appears to be only one finger dragging.
121124 ///
125+ /// If set to [MultitouchDragStrategy.averageBoundaryPointers] , all active
126+ /// pointers will be tracked, and the result is computed from the boundary pointers.
127+ ///
122128 /// If set to [MultitouchDragStrategy.sumAllPointers] ,
123129 /// all active pointers will be tracked together and the scrolling offset
124130 /// is the sum of the offsets of all active pointers
@@ -128,7 +134,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
128134 ///
129135 /// See also:
130136 ///
131- /// * [MultitouchDragStrategy] , which defines two different drag strategies for
137+ /// * [MultitouchDragStrategy] , which defines several different drag strategies for
132138 /// multi-finger drag.
133139 MultitouchDragStrategy multitouchDragStrategy;
134140
@@ -323,11 +329,27 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
323329
324330 Offset _getDeltaForDetails (Offset delta);
325331 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 ;
326337 bool _hasSufficientGlobalDistanceToAccept (PointerDeviceKind pointerDeviceKind, double ? deviceTouchSlop);
327338 bool _hasDragThresholdBeenMet = false ;
328339
329340 final Map <int , VelocityTracker > _velocityTrackers = < int , VelocityTracker > {};
330341
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+
331353 @override
332354 bool isPointerAllowed (PointerEvent event) {
333355 if (_initialButtons == null ) {
@@ -389,13 +411,194 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
389411 final bool result;
390412 switch (multitouchDragStrategy) {
391413 case MultitouchDragStrategy .sumAllPointers:
414+ case MultitouchDragStrategy .averageBoundaryPointers:
392415 result = true ;
393416 case MultitouchDragStrategy .latestPointer:
394- result = _acceptedActivePointers.length <= 1 || pointer == _acceptedActivePointers.last ;
417+ result = _activePointer == null || pointer == _activePointer ;
395418 }
396419 return result;
397420 }
398421
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+
399602 @override
400603 void handleEvent (PointerEvent event) {
401604 assert (_state != _DragState .ready);
@@ -424,6 +627,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
424627 final Offset position = (event is PointerMoveEvent ) ? event.position : (event.position + (event as PointerPanZoomUpdateEvent ).pan);
425628 final Offset localPosition = (event is PointerMoveEvent ) ? event.localPosition : (event.localPosition + (event as PointerPanZoomUpdateEvent ).localPan);
426629 _finalPosition = OffsetPair (local: localPosition, global: position);
630+ final Offset resolvedDelta = _resolveLocalDeltaForMultitouch (event.pointer, localDelta);
427631 switch (_state) {
428632 case _DragState .ready || _DragState .possible:
429633 _pendingDragOffset += OffsetPair (local: localDelta, global: delta);
@@ -447,24 +651,32 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
447651 case _DragState .accepted:
448652 _checkUpdate (
449653 sourceTimeStamp: event.timeStamp,
450- delta: _getDeltaForDetails (localDelta ),
451- primaryDelta: _getPrimaryValueFromOffset (localDelta ),
654+ delta: _getDeltaForDetails (resolvedDelta ),
655+ primaryDelta: _getPrimaryValueFromOffset (resolvedDelta ),
452656 globalPosition: position,
453657 localPosition: localPosition,
454658 );
455659 }
660+ _recordMoveDeltaForMultitouch (event.pointer, localDelta);
456661 }
457662 if (event case PointerUpEvent () || PointerCancelEvent () || PointerPanZoomEndEvent ()) {
458663 _giveUpPointer (event.pointer);
459664 }
460665 }
461666
462667 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;
463674
464675 @override
465676 void acceptGesture (int pointer) {
466677 assert (! _acceptedActivePointers.contains (pointer));
467678 _acceptedActivePointers.add (pointer);
679+ _activePointer = pointer;
468680 if (! onlyAcceptDragOnThreshold || _hasDragThresholdBeenMet) {
469681 _checkDrag (pointer);
470682 }
@@ -502,6 +714,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
502714 if (! _acceptedActivePointers.remove (pointer)) {
503715 resolvePointer (pointer, GestureDisposition .rejected);
504716 }
717+
718+ _moveDeltaBeforeFrame.remove (pointer);
719+ if (_activePointer == pointer) {
720+ _activePointer =
721+ _acceptedActivePointers.isNotEmpty ? _acceptedActivePointers.first : null ;
722+ }
505723 }
506724
507725 void _checkDown () {
@@ -687,6 +905,9 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
687905 @override
688906 double _getPrimaryValueFromOffset (Offset value) => value.dy;
689907
908+ @override
909+ _DragDirection ? _getPrimaryDragAxis () => _DragDirection .vertical;
910+
690911 @override
691912 String get debugDescription => 'vertical drag' ;
692913}
@@ -744,6 +965,9 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
744965 @override
745966 double _getPrimaryValueFromOffset (Offset value) => value.dx;
746967
968+ @override
969+ _DragDirection ? _getPrimaryDragAxis () => _DragDirection .horizontal;
970+
747971 @override
748972 String get debugDescription => 'horizontal drag' ;
749973}
@@ -801,3 +1025,8 @@ class PanGestureRecognizer extends DragGestureRecognizer {
8011025 @override
8021026 String get debugDescription => 'pan' ;
8031027}
1028+
1029+ enum _DragDirection {
1030+ horizontal,
1031+ vertical,
1032+ }
0 commit comments