Skip to content

Commit 7ddf42e

Browse files
authored
InteractiveViewer parameter to return to pre-3.3 trackpad/Magic Mouse behaviour (#114280)
* trackpadPanShouldActAsZoom * Address feedback * Move constant, add blank lines
1 parent 71f9207 commit 7ddf42e

File tree

5 files changed

+501
-79
lines changed

5 files changed

+501
-79
lines changed

packages/flutter/lib/src/gestures/scale.dart

+113-42
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ export 'events.dart' show PointerDownEvent, PointerEvent, PointerPanZoomStartEve
1515
export 'recognizer.dart' show DragStartBehavior;
1616
export 'velocity_tracker.dart' show Velocity;
1717

18+
/// The default conversion factor when treating mouse scrolling as scaling.
19+
///
20+
/// The value was arbitrarily chosen to feel natural for most mousewheels on
21+
/// all supported platforms.
22+
const double kDefaultMouseScrollToScaleFactor = 200;
23+
24+
/// The default conversion factor when treating trackpad scrolling as scaling.
25+
///
26+
/// This factor matches the default [kDefaultMouseScrollToScaleFactor] of 200 to
27+
/// feel natural for most trackpads, and the convention that scrolling up means
28+
/// zooming in.
29+
const Offset kDefaultTrackpadScrollToScaleFactor = Offset(0, -1/kDefaultMouseScrollToScaleFactor);
1830

1931
/// The possible states of a [ScaleGestureRecognizer].
2032
enum _ScaleState {
@@ -36,26 +48,61 @@ enum _ScaleState {
3648
}
3749

3850
class _PointerPanZoomData {
39-
_PointerPanZoomData({
40-
required this.focalPoint,
41-
required this.scale,
42-
required this.rotation
43-
});
44-
Offset focalPoint;
45-
double scale;
46-
double rotation;
51+
_PointerPanZoomData.fromStartEvent(
52+
this.parent,
53+
PointerPanZoomStartEvent event
54+
) : _position = event.position,
55+
_pan = Offset.zero,
56+
_scale = 1,
57+
_rotation = 0;
58+
59+
_PointerPanZoomData.fromUpdateEvent(
60+
this.parent,
61+
PointerPanZoomUpdateEvent event
62+
) : _position = event.position,
63+
_pan = event.pan,
64+
_scale = event.scale,
65+
_rotation = event.rotation;
66+
67+
final ScaleGestureRecognizer parent;
68+
final Offset _position;
69+
final Offset _pan;
70+
final double _scale;
71+
final double _rotation;
72+
73+
Offset get focalPoint {
74+
if (parent.trackpadScrollCausesScale) {
75+
return _position;
76+
}
77+
return _position + _pan;
78+
}
79+
80+
double get scale {
81+
if (parent.trackpadScrollCausesScale) {
82+
return _scale * math.exp(
83+
(_pan.dx * parent.trackpadScrollToScaleFactor.dx) +
84+
(_pan.dy * parent.trackpadScrollToScaleFactor.dy)
85+
);
86+
}
87+
return _scale;
88+
}
89+
90+
double get rotation => _rotation;
4791

4892
@override
49-
String toString() => '_PointerPanZoomData(focalPoint: $focalPoint, scale: $scale, angle: $rotation)';
93+
String toString() => '_PointerPanZoomData(parent: $parent, _position: $_position, _pan: $_pan, _scale: $_scale, _rotation: $_rotation)';
5094
}
5195

5296
/// Details for [GestureScaleStartCallback].
5397
class ScaleStartDetails {
5498
/// Creates details for [GestureScaleStartCallback].
5599
///
56100
/// The [focalPoint] argument must not be null.
57-
ScaleStartDetails({ this.focalPoint = Offset.zero, Offset? localFocalPoint, this.pointerCount = 0 })
58-
: assert(focalPoint != null), localFocalPoint = localFocalPoint ?? focalPoint;
101+
ScaleStartDetails({
102+
this.focalPoint = Offset.zero,
103+
Offset? localFocalPoint,
104+
this.pointerCount = 0,
105+
}) : assert(focalPoint != null), localFocalPoint = localFocalPoint ?? focalPoint;
59106

60107
/// The initial focal point of the pointers in contact with the screen.
61108
///
@@ -201,20 +248,23 @@ class ScaleEndDetails {
201248
/// Creates details for [GestureScaleEndCallback].
202249
///
203250
/// The [velocity] argument must not be null.
204-
ScaleEndDetails({ this.velocity = Velocity.zero, this.pointerCount = 0 })
251+
ScaleEndDetails({ this.velocity = Velocity.zero, this.scaleVelocity = 0, this.pointerCount = 0 })
205252
: assert(velocity != null);
206253

207254
/// The velocity of the last pointer to be lifted off of the screen.
208255
final Velocity velocity;
209256

257+
/// The final velocity of the scale factor reported by the gesture.
258+
final double scaleVelocity;
259+
210260
/// The number of pointers being tracked by the gesture recognizer.
211261
///
212262
/// Typically this is the number of fingers being used to pan the widget using the gesture
213263
/// recognizer.
214264
final int pointerCount;
215265

216266
@override
217-
String toString() => 'ScaleEndDetails(velocity: $velocity, pointerCount: $pointerCount)';
267+
String toString() => 'ScaleEndDetails(velocity: $velocity, scaleVelocity: $scaleVelocity, pointerCount: $pointerCount)';
218268
}
219269

220270
/// Signature for when the pointers in contact with the screen have established
@@ -285,6 +335,8 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
285335
super.kind,
286336
super.supportedDevices,
287337
this.dragStartBehavior = DragStartBehavior.down,
338+
this.trackpadScrollCausesScale = false,
339+
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
288340
}) : assert(dragStartBehavior != null);
289341

290342
/// Determines what point is used as the starting point in all calculations
@@ -332,6 +384,26 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
332384

333385
Matrix4? _lastTransform;
334386

387+
/// {@template flutter.gestures.scale.trackpadScrollCausesScale}
388+
/// Whether scrolling up/down on a trackpad should cause scaling instead of
389+
/// panning.
390+
///
391+
/// Defaults to false.
392+
/// {@endtemplate}
393+
bool trackpadScrollCausesScale;
394+
395+
/// {@template flutter.gestures.scale.trackpadScrollToScaleFactor}
396+
/// A factor to control the direction and magnitude of scale when converting
397+
/// trackpad scrolling.
398+
///
399+
/// Incoming trackpad pan offsets will be divided by this factor to get scale
400+
/// values. Increasing this offset will reduce the amount of scaling caused by
401+
/// a fixed amount of trackpad scrolling.
402+
///
403+
/// Defaults to [kDefaultTrackpadScrollToScaleFactor].
404+
/// {@endtemplate}
405+
Offset trackpadScrollToScaleFactor;
406+
335407
late Offset _initialFocalPoint;
336408
Offset? _currentFocalPoint;
337409
late double _initialSpan;
@@ -346,6 +418,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
346418
final Map<int, Offset> _pointerLocations = <int, Offset>{};
347419
final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance
348420
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
421+
VelocityTracker? _scaleVelocityTracker;
349422
late Offset _delta;
350423
final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
351424
double _initialPanZoomScaleFactor = 1;
@@ -466,23 +539,16 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
466539
_lastTransform = event.transform;
467540
} else if (event is PointerPanZoomStartEvent) {
468541
assert(_pointerPanZooms[event.pointer] == null);
469-
_pointerPanZooms[event.pointer] = _PointerPanZoomData(
470-
focalPoint: event.position,
471-
scale: 1,
472-
rotation: 0
473-
);
542+
_pointerPanZooms[event.pointer] = _PointerPanZoomData.fromStartEvent(this, event);
474543
didChangeConfiguration = true;
475544
shouldStartIfAccepted = true;
545+
_lastTransform = event.transform;
476546
} else if (event is PointerPanZoomUpdateEvent) {
477547
assert(_pointerPanZooms[event.pointer] != null);
478-
if (!event.synthesized) {
548+
if (!event.synthesized && !trackpadScrollCausesScale) {
479549
_velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
480550
}
481-
_pointerPanZooms[event.pointer] = _PointerPanZoomData(
482-
focalPoint: event.position + event.pan,
483-
scale: event.scale,
484-
rotation: event.rotation
485-
);
551+
_pointerPanZooms[event.pointer] = _PointerPanZoomData.fromUpdateEvent(this, event);
486552
_lastTransform = event.transform;
487553
shouldStartIfAccepted = true;
488554
} else if (event is PointerPanZoomEndEvent) {
@@ -495,7 +561,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
495561
_update();
496562

497563
if (!didChangeConfiguration || _reconfigure(event.pointer)) {
498-
_advanceStateMachine(shouldStartIfAccepted, event.kind);
564+
_advanceStateMachine(shouldStartIfAccepted, event);
499565
}
500566
stopTrackingIfPointerNoLongerDown(event);
501567
}
@@ -607,26 +673,28 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
607673
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) {
608674
velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
609675
}
610-
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerCount)));
676+
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
611677
} else {
612-
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount)));
678+
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
613679
}
614680
}
615681
_state = _ScaleState.accepted;
682+
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
616683
return false;
617684
}
685+
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
618686
return true;
619687
}
620688

621-
void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
689+
void _advanceStateMachine(bool shouldStartIfAccepted, PointerEvent event) {
622690
if (_state == _ScaleState.ready) {
623691
_state = _ScaleState.possible;
624692
}
625693

626694
if (_state == _ScaleState.possible) {
627695
final double spanDelta = (_currentSpan - _initialSpan).abs();
628696
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
629-
if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) {
697+
if (spanDelta > computeScaleSlop(event.kind) || focalPointDelta > computePanSlop(event.kind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) {
630698
resolve(GestureDisposition.accepted);
631699
}
632700
} else if (_state.index >= _ScaleState.accepted.index) {
@@ -638,19 +706,22 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
638706
_dispatchOnStartCallbackIfNeeded();
639707
}
640708

641-
if (_state == _ScaleState.started && onUpdate != null) {
642-
invokeCallback<void>('onUpdate', () {
643-
onUpdate!(ScaleUpdateDetails(
644-
scale: _scaleFactor,
645-
horizontalScale: _horizontalScaleFactor,
646-
verticalScale: _verticalScaleFactor,
647-
focalPoint: _currentFocalPoint!,
648-
localFocalPoint: _localFocalPoint,
649-
rotation: _computeRotationFactor(),
650-
pointerCount: _pointerCount,
651-
focalPointDelta: _delta,
652-
));
653-
});
709+
if (_state == _ScaleState.started) {
710+
_scaleVelocityTracker?.addPosition(event.timeStamp, Offset(_scaleFactor, 0));
711+
if (onUpdate != null) {
712+
invokeCallback<void>('onUpdate', () {
713+
onUpdate!(ScaleUpdateDetails(
714+
scale: _scaleFactor,
715+
horizontalScale: _horizontalScaleFactor,
716+
verticalScale: _verticalScaleFactor,
717+
focalPoint: _currentFocalPoint!,
718+
localFocalPoint: _localFocalPoint,
719+
rotation: _computeRotationFactor(),
720+
pointerCount: _pointerCount,
721+
focalPointDelta: _delta,
722+
));
723+
});
724+
}
654725
}
655726
}
656727

packages/flutter/lib/src/widgets/gesture_detector.dart

+11-1
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ class GestureDetector extends StatelessWidget {
288288
this.behavior,
289289
this.excludeFromSemantics = false,
290290
this.dragStartBehavior = DragStartBehavior.start,
291+
this.trackpadScrollCausesScale = false,
292+
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
291293
this.supportedDevices,
292294
}) : assert(excludeFromSemantics != null),
293295
assert(dragStartBehavior != null),
@@ -1014,6 +1016,12 @@ class GestureDetector extends StatelessWidget {
10141016
/// If set to null, events from all device types will be recognized. Defaults to null.
10151017
final Set<PointerDeviceKind>? supportedDevices;
10161018

1019+
/// {@macro flutter.gestures.scale.trackpadScrollCausesScale}
1020+
final bool trackpadScrollCausesScale;
1021+
1022+
/// {@macro flutter.gestures.scale.trackpadScrollToScaleFactor}
1023+
final Offset trackpadScrollToScaleFactor;
1024+
10171025
@override
10181026
Widget build(BuildContext context) {
10191027
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
@@ -1186,7 +1194,9 @@ class GestureDetector extends StatelessWidget {
11861194
..onUpdate = onScaleUpdate
11871195
..onEnd = onScaleEnd
11881196
..dragStartBehavior = dragStartBehavior
1189-
..gestureSettings = gestureSettings;
1197+
..gestureSettings = gestureSettings
1198+
..trackpadScrollCausesScale = trackpadScrollCausesScale
1199+
..trackpadScrollToScaleFactor = trackpadScrollToScaleFactor;
11901200
},
11911201
);
11921202
}

0 commit comments

Comments
 (0)