Skip to content

Commit 8284ad7

Browse files
Added ability to lock panning to vertical or horizontal based on initial pan direction (Resolves #18) (#19)
1 parent 421929e commit 8284ad7

File tree

2 files changed

+101
-21
lines changed

2 files changed

+101
-21
lines changed

example/lib/main_list.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
5252
return Scaffold(
5353
body: PageListViewportGestures(
5454
controller: _controller,
55+
lockPanAxis: true,
5556
child: PageListViewport(
5657
controller: _controller,
5758
pageCount: 60, //_pageCount,

lib/src/page_list_viewport_gestures.dart

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:math';
2+
13
import 'package:flutter/gestures.dart';
24
import 'package:flutter/physics.dart';
35
import 'package:flutter/scheduler.dart';
@@ -19,6 +21,7 @@ class PageListViewportGestures extends StatefulWidget {
1921
this.onDoubleTapDown,
2022
this.onDoubleTap,
2123
this.onDoubleTapCancel,
24+
this.lockPanAxis = false,
2225
this.clock = const Clock(),
2326
required this.child,
2427
}) : super(key: key);
@@ -37,6 +40,12 @@ class PageListViewportGestures extends StatefulWidget {
3740
final void Function()? onDoubleTap;
3841
final void Function()? onDoubleTapCancel;
3942

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+
4049
/// Reports the time, so that the gesture system can track how much
4150
/// time has passed.
4251
///
@@ -50,7 +59,15 @@ class PageListViewportGestures extends StatefulWidget {
5059

5160
class _PageListViewportGesturesState extends State<PageListViewportGestures> with TickerProviderStateMixin {
5261
bool _isPanningEnabled = true;
62+
5363
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+
5471
late DeprecatedPanAndScaleVelocityTracker _panAndScaleVelocityTracker;
5572
double? _startContentScale;
5673
Offset? _startOffset;
@@ -95,20 +112,20 @@ class _PageListViewportGesturesState extends State<PageListViewportGestures> wit
95112
return;
96113
}
97114
_isPanning = true;
115+
98116
final timeSinceLastGesture = _endTimeInMillis != null ? _timeSinceEndOfLastGesture : null;
99117
_startContentScale = widget.controller.scale;
100118
_startOffset = widget.controller.origin;
101-
_referenceFocalPoint = details.localFocalPoint;
119+
_panAndScaleFocalPoint = details.localFocalPoint;
102120
_panAndScaleVelocityTracker.onScaleStart(details);
121+
103122
if ((timeSinceLastGesture == null || timeSinceLastGesture > const Duration(milliseconds: 30))) {
104123
// We've started a new gesture after a reasonable period of time since the
105124
// last gesture. Stop any momentum from the last gesture.
106125
_stopMomentum();
107126
}
108127
}
109128

110-
Offset? _referenceFocalPoint; // Point where the current gesture began.
111-
112129
void _onScaleUpdate(ScaleUpdateDetails details) {
113130
PageListViewportLogs.pagesList
114131
.finer("onScaleUpdate() - new focal point ${details.focalPoint}, focal delta: ${details.focalPointDelta}");
@@ -117,6 +134,7 @@ class _PageListViewportGesturesState extends State<PageListViewportGestures> wit
117134
// or scale with a stylus.
118135
return;
119136
}
137+
120138
if (!_isPanningEnabled) {
121139
PageListViewportLogs.pagesListGestures.finer("Started panning when the stylus was down. Resetting transform to:");
122140
PageListViewportLogs.pagesListGestures.finer(" - origin: ${widget.controller.origin}");
@@ -133,28 +151,90 @@ class _PageListViewportGesturesState extends State<PageListViewportGestures> wit
133151
..translate(_startOffset! - widget.controller.origin);
134152
return;
135153
}
136-
_panAndScaleVelocityTracker.onScaleUpdate(details);
154+
137155
// Translate so that the same point in the scene is underneath the
138156
// 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+
141165
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+
144171
PageListViewportLogs.pagesListGestures
145172
.finer("New origin: ${widget.controller.origin}, scale: ${widget.controller.scale}");
146173
}
147174

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+
148210
void _onScaleEnd(ScaleEndDetails details) {
149211
PageListViewportLogs.pagesListGestures.finer("onScaleEnd()");
150212
if (!_isPanning) {
151213
return;
152214
}
153215

154-
_panAndScaleVelocityTracker.onScaleEnd(details);
216+
final velocity = _restrictVectorToAxisIfDesired(details.velocity.pixelsPerSecond);
217+
_panAndScaleVelocityTracker.onScaleEnd(velocity, details.pointerCount);
155218
if (details.pointerCount == 0) {
156219
_startMomentum();
157220
_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;
158238
}
159239
}
160240

@@ -298,11 +378,11 @@ class DeprecatedPanAndScaleVelocityTracker {
298378
}
299379

300380
_previousPointerCount = details.pointerCount;
301-
_startPosition = details.focalPoint;
381+
_startPosition = details.localFocalPoint;
302382
}
303383

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");
306386

307387
if (_isPossibleGestureContinuation) {
308388
if (_timeSinceStartOfGesture < const Duration(milliseconds: 24)) {
@@ -316,27 +396,27 @@ class DeprecatedPanAndScaleVelocityTracker {
316396
PageListViewportLogs.pagesListGestures
317397
.fine(" - a possible gesture continuation has been confirmed as a new gesture. Restarting velocity.");
318398
_currentGestureStartTimeInMillis = _clock.millis;
319-
_previousGesturePointerCount = details.pointerCount;
399+
_previousGesturePointerCount = pointerCount;
320400
_launchVelocity = Offset.zero;
321401

322402
_isPossibleGestureContinuation = false;
323403
}
324404

325-
_lastFocalPosition = details.focalPoint;
405+
_lastFocalPosition = localFocalPoint;
326406
}
327407

328-
void onScaleEnd(ScaleEndDetails details) {
408+
void onScaleEnd(Offset velocity, int pointerCount) {
329409
final gestureDuration = Duration(milliseconds: _clock.millis - _currentGestureStartTimeInMillis!);
330410
PageListViewportLogs.pagesListGestures.fine("onScaleEnd() - gesture duration: ${gestureDuration.inMilliseconds}");
331411

332412
_previousGestureEndTimeInMillis = _clock.millis;
333-
_previousPointerCount = details.pointerCount;
413+
_previousPointerCount = pointerCount;
334414
_currentGestureStartAction = null;
335415
_currentGestureStartTimeInMillis = null;
336416

337417
if (_isPossibleGestureContinuation) {
338418
PageListViewportLogs.pagesListGestures.fine(" - this gesture is a continuation of a previous gesture.");
339-
if (details.pointerCount > 0) {
419+
if (pointerCount > 0) {
340420
PageListViewportLogs.pagesListGestures.fine(
341421
" - this continuation gesture still has fingers touching the screen. The end of this gesture means nothing for the velocity.");
342422
return;
@@ -364,22 +444,21 @@ class DeprecatedPanAndScaleVelocityTracker {
364444
}
365445

366446
final translationDistance = (_lastFocalPosition - _startPosition).distance;
367-
if (translationDistance > kViewportMinFlingDistance &&
368-
details.velocity.pixelsPerSecond.distance < kViewportMinFlingVelocity) {
447+
if (translationDistance > kViewportMinFlingDistance && velocity.distance < kViewportMinFlingVelocity) {
369448
// The user was readjusting the viewport by dragging it to the
370449
// new position.
371450
return;
372451
}
373452

374-
if (details.pointerCount > 0) {
453+
if (pointerCount > 0) {
375454
PageListViewportLogs.pagesListGestures
376455
.fine(" - the user removed a finger, but is still interacting. Storing velocity for later.");
377456
PageListViewportLogs.pagesListGestures
378457
.fine(" - stored velocity: $_launchVelocity, magnitude: ${_launchVelocity.distance}");
379458
return;
380459
}
381460

382-
_launchVelocity = details.velocity.pixelsPerSecond;
461+
_launchVelocity = velocity;
383462
PageListViewportLogs.pagesListGestures
384463
.fine(" - the user has completely stopped interacting. Launch velocity is: $_launchVelocity");
385464
}

0 commit comments

Comments
 (0)