1
+ import 'dart:math' ;
2
+
1
3
import 'package:flutter/gestures.dart' ;
2
4
import 'package:flutter/physics.dart' ;
3
5
import 'package:flutter/scheduler.dart' ;
@@ -19,6 +21,7 @@ class PageListViewportGestures extends StatefulWidget {
19
21
this .onDoubleTapDown,
20
22
this .onDoubleTap,
21
23
this .onDoubleTapCancel,
24
+ this .lockPanAxis = false ,
22
25
this .clock = const Clock (),
23
26
required this .child,
24
27
}) : super (key: key);
@@ -37,6 +40,12 @@ class PageListViewportGestures extends StatefulWidget {
37
40
final void Function ()? onDoubleTap;
38
41
final void Function ()? onDoubleTapCancel;
39
42
43
+ /// Whether the user should be locked into horizontal or vertical scrolling,
44
+ /// when the user pans roughly in those directions.
45
+ ///
46
+ /// When the user drags near 45 degrees, the user retains full pan control.
47
+ final bool lockPanAxis;
48
+
40
49
/// Reports the time, so that the gesture system can track how much
41
50
/// time has passed.
42
51
///
@@ -50,7 +59,15 @@ class PageListViewportGestures extends StatefulWidget {
50
59
51
60
class _PageListViewportGesturesState extends State <PageListViewportGestures > with TickerProviderStateMixin {
52
61
bool _isPanningEnabled = true ;
62
+
53
63
bool _isPanning = false ;
64
+
65
+ bool _hasChosenWhetherToLock = false ;
66
+ bool _isLockedHorizontal = false ;
67
+ bool _isLockedVertical = false ;
68
+
69
+ Offset ? _panAndScaleFocalPoint; // Point where the current gesture began.
70
+
54
71
late DeprecatedPanAndScaleVelocityTracker _panAndScaleVelocityTracker;
55
72
double ? _startContentScale;
56
73
Offset ? _startOffset;
@@ -95,20 +112,20 @@ class _PageListViewportGesturesState extends State<PageListViewportGestures> wit
95
112
return ;
96
113
}
97
114
_isPanning = true ;
115
+
98
116
final timeSinceLastGesture = _endTimeInMillis != null ? _timeSinceEndOfLastGesture : null ;
99
117
_startContentScale = widget.controller.scale;
100
118
_startOffset = widget.controller.origin;
101
- _referenceFocalPoint = details.localFocalPoint;
119
+ _panAndScaleFocalPoint = details.localFocalPoint;
102
120
_panAndScaleVelocityTracker.onScaleStart (details);
121
+
103
122
if ((timeSinceLastGesture == null || timeSinceLastGesture > const Duration (milliseconds: 30 ))) {
104
123
// We've started a new gesture after a reasonable period of time since the
105
124
// last gesture. Stop any momentum from the last gesture.
106
125
_stopMomentum ();
107
126
}
108
127
}
109
128
110
- Offset ? _referenceFocalPoint; // Point where the current gesture began.
111
-
112
129
void _onScaleUpdate (ScaleUpdateDetails details) {
113
130
PageListViewportLogs .pagesList
114
131
.finer ("onScaleUpdate() - new focal point ${details .focalPoint }, focal delta: ${details .focalPointDelta }" );
@@ -117,6 +134,7 @@ class _PageListViewportGesturesState extends State<PageListViewportGestures> wit
117
134
// or scale with a stylus.
118
135
return ;
119
136
}
137
+
120
138
if (! _isPanningEnabled) {
121
139
PageListViewportLogs .pagesListGestures.finer ("Started panning when the stylus was down. Resetting transform to:" );
122
140
PageListViewportLogs .pagesListGestures.finer (" - origin: ${widget .controller .origin }" );
@@ -133,28 +151,90 @@ class _PageListViewportGesturesState extends State<PageListViewportGestures> wit
133
151
..translate (_startOffset! - widget.controller.origin);
134
152
return ;
135
153
}
136
- _panAndScaleVelocityTracker. onScaleUpdate (details);
154
+
137
155
// Translate so that the same point in the scene is underneath the
138
156
// focal point before and after the movement.
139
- final Offset translationChange = details.localFocalPoint - _referenceFocalPoint! ;
140
- _referenceFocalPoint = details.localFocalPoint;
157
+ Offset focalPointTranslation = details.localFocalPoint - _panAndScaleFocalPoint! ;
158
+
159
+ // (Maybe) Axis locking.
160
+ _lockPanningAxisIfDesired (focalPointTranslation, details.pointerCount);
161
+ focalPointTranslation = _restrictVectorToAxisIfDesired (focalPointTranslation);
162
+
163
+ _panAndScaleFocalPoint = _panAndScaleFocalPoint! + focalPointTranslation;
164
+
141
165
widget.controller //
142
- ..setScale (details.scale * _startContentScale! , details.localFocalPoint)
143
- ..translate (translationChange);
166
+ ..setScale (details.scale * _startContentScale! , _panAndScaleFocalPoint! )
167
+ ..translate (focalPointTranslation);
168
+
169
+ _panAndScaleVelocityTracker.onScaleUpdate (_panAndScaleFocalPoint! , details.pointerCount);
170
+
144
171
PageListViewportLogs .pagesListGestures
145
172
.finer ("New origin: ${widget .controller .origin }, scale: ${widget .controller .scale }" );
146
173
}
147
174
175
+ void _lockPanningAxisIfDesired (Offset translation, int pointerCount) {
176
+ if (_hasChosenWhetherToLock) {
177
+ // We've already made our locking decision. Fizzle.
178
+ return ;
179
+ }
180
+
181
+ if (translation.distance < 0.000001 ) {
182
+ // This means a distance of zero. We can't make a decision with zero translation.
183
+ // Fizzle and wait for the next panning notification.
184
+ return ;
185
+ }
186
+
187
+ _hasChosenWhetherToLock = true ;
188
+
189
+ if (! widget.lockPanAxis) {
190
+ // The developer explicitly requested no locked panning.
191
+ return ;
192
+ }
193
+ if (pointerCount > 1 ) {
194
+ // We don't lock axis direction when scaling with 2 fingers.
195
+ return ;
196
+ }
197
+
198
+ // Choose to lock in a particular axis direction, or not.
199
+ final movementAngle = translation.direction;
200
+ final movementAnglePositive = movementAngle.abs ();
201
+ if (((2 / 6 ) * pi < movementAnglePositive) && (movementAnglePositive < (4 / 6 ) * pi)) {
202
+ PageListViewportLogs .pagesListGestures.finer ("Locking panning into vertical-only movement." );
203
+ _isLockedVertical = true ;
204
+ } else if (movementAnglePositive < (1 / 6 ) * pi || movementAnglePositive > (5 / 6 ) * pi) {
205
+ PageListViewportLogs .pagesListGestures.finer ("Locking panning into horizontal-only movement." );
206
+ _isLockedHorizontal = true ;
207
+ }
208
+ }
209
+
148
210
void _onScaleEnd (ScaleEndDetails details) {
149
211
PageListViewportLogs .pagesListGestures.finer ("onScaleEnd()" );
150
212
if (! _isPanning) {
151
213
return ;
152
214
}
153
215
154
- _panAndScaleVelocityTracker.onScaleEnd (details);
216
+ final velocity = _restrictVectorToAxisIfDesired (details.velocity.pixelsPerSecond);
217
+ _panAndScaleVelocityTracker.onScaleEnd (velocity, details.pointerCount);
155
218
if (details.pointerCount == 0 ) {
156
219
_startMomentum ();
157
220
_isPanning = false ;
221
+ _hasChosenWhetherToLock = false ;
222
+ _isLockedHorizontal = false ;
223
+ _isLockedVertical = false ;
224
+ }
225
+ }
226
+
227
+ /// (Maybe) Restricts a 2D vector to a single axis of motion, e.g., restricts a translation
228
+ /// vector, or a velocity vector to just horizontal or vertical motion.
229
+ ///
230
+ /// Restriction is based on the current state of [_isLockedHorizontal] and [_isLockedVertical] .
231
+ Offset _restrictVectorToAxisIfDesired (Offset rawVector) {
232
+ if (_isLockedHorizontal) {
233
+ return Offset (rawVector.dx, 0.0 );
234
+ } else if (_isLockedVertical) {
235
+ return Offset (0.0 , rawVector.dy);
236
+ } else {
237
+ return rawVector;
158
238
}
159
239
}
160
240
@@ -298,11 +378,11 @@ class DeprecatedPanAndScaleVelocityTracker {
298
378
}
299
379
300
380
_previousPointerCount = details.pointerCount;
301
- _startPosition = details.focalPoint ;
381
+ _startPosition = details.localFocalPoint ;
302
382
}
303
383
304
- void onScaleUpdate (ScaleUpdateDetails details ) {
305
- PageListViewportLogs .pagesListGestures.fine ("Scale update: ${ details . localFocalPoint } " );
384
+ void onScaleUpdate (Offset localFocalPoint, int pointerCount ) {
385
+ PageListViewportLogs .pagesListGestures.fine ("Scale update: $localFocalPoint " );
306
386
307
387
if (_isPossibleGestureContinuation) {
308
388
if (_timeSinceStartOfGesture < const Duration (milliseconds: 24 )) {
@@ -316,27 +396,27 @@ class DeprecatedPanAndScaleVelocityTracker {
316
396
PageListViewportLogs .pagesListGestures
317
397
.fine (" - a possible gesture continuation has been confirmed as a new gesture. Restarting velocity." );
318
398
_currentGestureStartTimeInMillis = _clock.millis;
319
- _previousGesturePointerCount = details. pointerCount;
399
+ _previousGesturePointerCount = pointerCount;
320
400
_launchVelocity = Offset .zero;
321
401
322
402
_isPossibleGestureContinuation = false ;
323
403
}
324
404
325
- _lastFocalPosition = details.focalPoint ;
405
+ _lastFocalPosition = localFocalPoint ;
326
406
}
327
407
328
- void onScaleEnd (ScaleEndDetails details ) {
408
+ void onScaleEnd (Offset velocity, int pointerCount ) {
329
409
final gestureDuration = Duration (milliseconds: _clock.millis - _currentGestureStartTimeInMillis! );
330
410
PageListViewportLogs .pagesListGestures.fine ("onScaleEnd() - gesture duration: ${gestureDuration .inMilliseconds }" );
331
411
332
412
_previousGestureEndTimeInMillis = _clock.millis;
333
- _previousPointerCount = details. pointerCount;
413
+ _previousPointerCount = pointerCount;
334
414
_currentGestureStartAction = null ;
335
415
_currentGestureStartTimeInMillis = null ;
336
416
337
417
if (_isPossibleGestureContinuation) {
338
418
PageListViewportLogs .pagesListGestures.fine (" - this gesture is a continuation of a previous gesture." );
339
- if (details. pointerCount > 0 ) {
419
+ if (pointerCount > 0 ) {
340
420
PageListViewportLogs .pagesListGestures.fine (
341
421
" - this continuation gesture still has fingers touching the screen. The end of this gesture means nothing for the velocity." );
342
422
return ;
@@ -364,22 +444,21 @@ class DeprecatedPanAndScaleVelocityTracker {
364
444
}
365
445
366
446
final translationDistance = (_lastFocalPosition - _startPosition).distance;
367
- if (translationDistance > kViewportMinFlingDistance &&
368
- details.velocity.pixelsPerSecond.distance < kViewportMinFlingVelocity) {
447
+ if (translationDistance > kViewportMinFlingDistance && velocity.distance < kViewportMinFlingVelocity) {
369
448
// The user was readjusting the viewport by dragging it to the
370
449
// new position.
371
450
return ;
372
451
}
373
452
374
- if (details. pointerCount > 0 ) {
453
+ if (pointerCount > 0 ) {
375
454
PageListViewportLogs .pagesListGestures
376
455
.fine (" - the user removed a finger, but is still interacting. Storing velocity for later." );
377
456
PageListViewportLogs .pagesListGestures
378
457
.fine (" - stored velocity: $_launchVelocity , magnitude: ${_launchVelocity .distance }" );
379
458
return ;
380
459
}
381
460
382
- _launchVelocity = details. velocity.pixelsPerSecond ;
461
+ _launchVelocity = velocity;
383
462
PageListViewportLogs .pagesListGestures
384
463
.fine (" - the user has completely stopped interacting. Launch velocity is: $_launchVelocity " );
385
464
}
0 commit comments