Unverified Commit 7ddf42ea authored by Callum Moffat's avatar Callum Moffat Committed by GitHub

InteractiveViewer parameter to return to pre-3.3 trackpad/Magic Mouse behaviour (#114280)

* trackpadPanShouldActAsZoom

* Address feedback

* Move constant, add blank lines
parent 71f92073
...@@ -15,6 +15,18 @@ export 'events.dart' show PointerDownEvent, PointerEvent, PointerPanZoomStartEve ...@@ -15,6 +15,18 @@ export 'events.dart' show PointerDownEvent, PointerEvent, PointerPanZoomStartEve
export 'recognizer.dart' show DragStartBehavior; export 'recognizer.dart' show DragStartBehavior;
export 'velocity_tracker.dart' show Velocity; export 'velocity_tracker.dart' show Velocity;
/// The default conversion factor when treating mouse scrolling as scaling.
///
/// The value was arbitrarily chosen to feel natural for most mousewheels on
/// all supported platforms.
const double kDefaultMouseScrollToScaleFactor = 200;
/// The default conversion factor when treating trackpad scrolling as scaling.
///
/// This factor matches the default [kDefaultMouseScrollToScaleFactor] of 200 to
/// feel natural for most trackpads, and the convention that scrolling up means
/// zooming in.
const Offset kDefaultTrackpadScrollToScaleFactor = Offset(0, -1/kDefaultMouseScrollToScaleFactor);
/// The possible states of a [ScaleGestureRecognizer]. /// The possible states of a [ScaleGestureRecognizer].
enum _ScaleState { enum _ScaleState {
...@@ -36,17 +48,49 @@ enum _ScaleState { ...@@ -36,17 +48,49 @@ enum _ScaleState {
} }
class _PointerPanZoomData { class _PointerPanZoomData {
_PointerPanZoomData({ _PointerPanZoomData.fromStartEvent(
required this.focalPoint, this.parent,
required this.scale, PointerPanZoomStartEvent event
required this.rotation ) : _position = event.position,
}); _pan = Offset.zero,
Offset focalPoint; _scale = 1,
double scale; _rotation = 0;
double rotation;
_PointerPanZoomData.fromUpdateEvent(
this.parent,
PointerPanZoomUpdateEvent event
) : _position = event.position,
_pan = event.pan,
_scale = event.scale,
_rotation = event.rotation;
final ScaleGestureRecognizer parent;
final Offset _position;
final Offset _pan;
final double _scale;
final double _rotation;
Offset get focalPoint {
if (parent.trackpadScrollCausesScale) {
return _position;
}
return _position + _pan;
}
double get scale {
if (parent.trackpadScrollCausesScale) {
return _scale * math.exp(
(_pan.dx * parent.trackpadScrollToScaleFactor.dx) +
(_pan.dy * parent.trackpadScrollToScaleFactor.dy)
);
}
return _scale;
}
double get rotation => _rotation;
@override @override
String toString() => '_PointerPanZoomData(focalPoint: $focalPoint, scale: $scale, angle: $rotation)'; String toString() => '_PointerPanZoomData(parent: $parent, _position: $_position, _pan: $_pan, _scale: $_scale, _rotation: $_rotation)';
} }
/// Details for [GestureScaleStartCallback]. /// Details for [GestureScaleStartCallback].
...@@ -54,8 +98,11 @@ class ScaleStartDetails { ...@@ -54,8 +98,11 @@ class ScaleStartDetails {
/// Creates details for [GestureScaleStartCallback]. /// Creates details for [GestureScaleStartCallback].
/// ///
/// The [focalPoint] argument must not be null. /// The [focalPoint] argument must not be null.
ScaleStartDetails({ this.focalPoint = Offset.zero, Offset? localFocalPoint, this.pointerCount = 0 }) ScaleStartDetails({
: assert(focalPoint != null), localFocalPoint = localFocalPoint ?? focalPoint; this.focalPoint = Offset.zero,
Offset? localFocalPoint,
this.pointerCount = 0,
}) : assert(focalPoint != null), localFocalPoint = localFocalPoint ?? focalPoint;
/// The initial focal point of the pointers in contact with the screen. /// The initial focal point of the pointers in contact with the screen.
/// ///
...@@ -201,12 +248,15 @@ class ScaleEndDetails { ...@@ -201,12 +248,15 @@ class ScaleEndDetails {
/// Creates details for [GestureScaleEndCallback]. /// Creates details for [GestureScaleEndCallback].
/// ///
/// The [velocity] argument must not be null. /// The [velocity] argument must not be null.
ScaleEndDetails({ this.velocity = Velocity.zero, this.pointerCount = 0 }) ScaleEndDetails({ this.velocity = Velocity.zero, this.scaleVelocity = 0, this.pointerCount = 0 })
: assert(velocity != null); : assert(velocity != null);
/// The velocity of the last pointer to be lifted off of the screen. /// The velocity of the last pointer to be lifted off of the screen.
final Velocity velocity; final Velocity velocity;
/// The final velocity of the scale factor reported by the gesture.
final double scaleVelocity;
/// The number of pointers being tracked by the gesture recognizer. /// The number of pointers being tracked by the gesture recognizer.
/// ///
/// Typically this is the number of fingers being used to pan the widget using the gesture /// Typically this is the number of fingers being used to pan the widget using the gesture
...@@ -214,7 +264,7 @@ class ScaleEndDetails { ...@@ -214,7 +264,7 @@ class ScaleEndDetails {
final int pointerCount; final int pointerCount;
@override @override
String toString() => 'ScaleEndDetails(velocity: $velocity, pointerCount: $pointerCount)'; String toString() => 'ScaleEndDetails(velocity: $velocity, scaleVelocity: $scaleVelocity, pointerCount: $pointerCount)';
} }
/// Signature for when the pointers in contact with the screen have established /// Signature for when the pointers in contact with the screen have established
...@@ -285,6 +335,8 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -285,6 +335,8 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
super.kind, super.kind,
super.supportedDevices, super.supportedDevices,
this.dragStartBehavior = DragStartBehavior.down, this.dragStartBehavior = DragStartBehavior.down,
this.trackpadScrollCausesScale = false,
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
}) : assert(dragStartBehavior != null); }) : assert(dragStartBehavior != null);
/// Determines what point is used as the starting point in all calculations /// Determines what point is used as the starting point in all calculations
...@@ -332,6 +384,26 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -332,6 +384,26 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
Matrix4? _lastTransform; Matrix4? _lastTransform;
/// {@template flutter.gestures.scale.trackpadScrollCausesScale}
/// Whether scrolling up/down on a trackpad should cause scaling instead of
/// panning.
///
/// Defaults to false.
/// {@endtemplate}
bool trackpadScrollCausesScale;
/// {@template flutter.gestures.scale.trackpadScrollToScaleFactor}
/// A factor to control the direction and magnitude of scale when converting
/// trackpad scrolling.
///
/// Incoming trackpad pan offsets will be divided by this factor to get scale
/// values. Increasing this offset will reduce the amount of scaling caused by
/// a fixed amount of trackpad scrolling.
///
/// Defaults to [kDefaultTrackpadScrollToScaleFactor].
/// {@endtemplate}
Offset trackpadScrollToScaleFactor;
late Offset _initialFocalPoint; late Offset _initialFocalPoint;
Offset? _currentFocalPoint; Offset? _currentFocalPoint;
late double _initialSpan; late double _initialSpan;
...@@ -346,6 +418,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -346,6 +418,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
final Map<int, Offset> _pointerLocations = <int, Offset>{}; final Map<int, Offset> _pointerLocations = <int, Offset>{};
final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{}; final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
VelocityTracker? _scaleVelocityTracker;
late Offset _delta; late Offset _delta;
final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{}; final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
double _initialPanZoomScaleFactor = 1; double _initialPanZoomScaleFactor = 1;
...@@ -466,23 +539,16 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -466,23 +539,16 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_lastTransform = event.transform; _lastTransform = event.transform;
} else if (event is PointerPanZoomStartEvent) { } else if (event is PointerPanZoomStartEvent) {
assert(_pointerPanZooms[event.pointer] == null); assert(_pointerPanZooms[event.pointer] == null);
_pointerPanZooms[event.pointer] = _PointerPanZoomData( _pointerPanZooms[event.pointer] = _PointerPanZoomData.fromStartEvent(this, event);
focalPoint: event.position,
scale: 1,
rotation: 0
);
didChangeConfiguration = true; didChangeConfiguration = true;
shouldStartIfAccepted = true; shouldStartIfAccepted = true;
_lastTransform = event.transform;
} else if (event is PointerPanZoomUpdateEvent) { } else if (event is PointerPanZoomUpdateEvent) {
assert(_pointerPanZooms[event.pointer] != null); assert(_pointerPanZooms[event.pointer] != null);
if (!event.synthesized) { if (!event.synthesized && !trackpadScrollCausesScale) {
_velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan); _velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
} }
_pointerPanZooms[event.pointer] = _PointerPanZoomData( _pointerPanZooms[event.pointer] = _PointerPanZoomData.fromUpdateEvent(this, event);
focalPoint: event.position + event.pan,
scale: event.scale,
rotation: event.rotation
);
_lastTransform = event.transform; _lastTransform = event.transform;
shouldStartIfAccepted = true; shouldStartIfAccepted = true;
} else if (event is PointerPanZoomEndEvent) { } else if (event is PointerPanZoomEndEvent) {
...@@ -495,7 +561,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -495,7 +561,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_update(); _update();
if (!didChangeConfiguration || _reconfigure(event.pointer)) { if (!didChangeConfiguration || _reconfigure(event.pointer)) {
_advanceStateMachine(shouldStartIfAccepted, event.kind); _advanceStateMachine(shouldStartIfAccepted, event);
} }
stopTrackingIfPointerNoLongerDown(event); stopTrackingIfPointerNoLongerDown(event);
} }
...@@ -607,18 +673,20 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -607,18 +673,20 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) { if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) {
velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity); velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
} }
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerCount))); invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
} else { } else {
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount))); invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
} }
} }
_state = _ScaleState.accepted; _state = _ScaleState.accepted;
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
return false; return false;
} }
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
return true; return true;
} }
void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) { void _advanceStateMachine(bool shouldStartIfAccepted, PointerEvent event) {
if (_state == _ScaleState.ready) { if (_state == _ScaleState.ready) {
_state = _ScaleState.possible; _state = _ScaleState.possible;
} }
...@@ -626,7 +694,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -626,7 +694,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
if (_state == _ScaleState.possible) { if (_state == _ScaleState.possible) {
final double spanDelta = (_currentSpan - _initialSpan).abs(); final double spanDelta = (_currentSpan - _initialSpan).abs();
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance; final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) { if (spanDelta > computeScaleSlop(event.kind) || focalPointDelta > computePanSlop(event.kind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) {
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} }
} else if (_state.index >= _ScaleState.accepted.index) { } else if (_state.index >= _ScaleState.accepted.index) {
...@@ -638,19 +706,22 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -638,19 +706,22 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_dispatchOnStartCallbackIfNeeded(); _dispatchOnStartCallbackIfNeeded();
} }
if (_state == _ScaleState.started && onUpdate != null) { if (_state == _ScaleState.started) {
invokeCallback<void>('onUpdate', () { _scaleVelocityTracker?.addPosition(event.timeStamp, Offset(_scaleFactor, 0));
onUpdate!(ScaleUpdateDetails( if (onUpdate != null) {
scale: _scaleFactor, invokeCallback<void>('onUpdate', () {
horizontalScale: _horizontalScaleFactor, onUpdate!(ScaleUpdateDetails(
verticalScale: _verticalScaleFactor, scale: _scaleFactor,
focalPoint: _currentFocalPoint!, horizontalScale: _horizontalScaleFactor,
localFocalPoint: _localFocalPoint, verticalScale: _verticalScaleFactor,
rotation: _computeRotationFactor(), focalPoint: _currentFocalPoint!,
pointerCount: _pointerCount, localFocalPoint: _localFocalPoint,
focalPointDelta: _delta, rotation: _computeRotationFactor(),
)); pointerCount: _pointerCount,
}); focalPointDelta: _delta,
));
});
}
} }
} }
......
...@@ -288,6 +288,8 @@ class GestureDetector extends StatelessWidget { ...@@ -288,6 +288,8 @@ class GestureDetector extends StatelessWidget {
this.behavior, this.behavior,
this.excludeFromSemantics = false, this.excludeFromSemantics = false,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.trackpadScrollCausesScale = false,
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
this.supportedDevices, this.supportedDevices,
}) : assert(excludeFromSemantics != null), }) : assert(excludeFromSemantics != null),
assert(dragStartBehavior != null), assert(dragStartBehavior != null),
...@@ -1014,6 +1016,12 @@ class GestureDetector extends StatelessWidget { ...@@ -1014,6 +1016,12 @@ class GestureDetector extends StatelessWidget {
/// If set to null, events from all device types will be recognized. Defaults to null. /// If set to null, events from all device types will be recognized. Defaults to null.
final Set<PointerDeviceKind>? supportedDevices; final Set<PointerDeviceKind>? supportedDevices;
/// {@macro flutter.gestures.scale.trackpadScrollCausesScale}
final bool trackpadScrollCausesScale;
/// {@macro flutter.gestures.scale.trackpadScrollToScaleFactor}
final Offset trackpadScrollToScaleFactor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{}; final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
...@@ -1186,7 +1194,9 @@ class GestureDetector extends StatelessWidget { ...@@ -1186,7 +1194,9 @@ class GestureDetector extends StatelessWidget {
..onUpdate = onScaleUpdate ..onUpdate = onScaleUpdate
..onEnd = onScaleEnd ..onEnd = onScaleEnd
..dragStartBehavior = dragStartBehavior ..dragStartBehavior = dragStartBehavior
..gestureSettings = gestureSettings; ..gestureSettings = gestureSettings
..trackpadScrollCausesScale = trackpadScrollCausesScale
..trackpadScrollToScaleFactor = trackpadScrollToScaleFactor;
}, },
); );
} }
......
...@@ -85,9 +85,10 @@ class InteractiveViewer extends StatefulWidget { ...@@ -85,9 +85,10 @@ class InteractiveViewer extends StatefulWidget {
this.onInteractionUpdate, this.onInteractionUpdate,
this.panEnabled = true, this.panEnabled = true,
this.scaleEnabled = true, this.scaleEnabled = true,
this.scaleFactor = 200.0, this.scaleFactor = kDefaultMouseScrollToScaleFactor,
this.transformationController, this.transformationController,
this.alignment, this.alignment,
this.trackpadScrollCausesScale = false,
required Widget this.child, required Widget this.child,
}) : assert(alignPanAxis != null), }) : assert(alignPanAxis != null),
assert(panAxis != null), assert(panAxis != null),
...@@ -103,6 +104,7 @@ class InteractiveViewer extends StatefulWidget { ...@@ -103,6 +104,7 @@ class InteractiveViewer extends StatefulWidget {
assert(maxScale >= minScale), assert(maxScale >= minScale),
assert(panEnabled != null), assert(panEnabled != null),
assert(scaleEnabled != null), assert(scaleEnabled != null),
assert(trackpadScrollCausesScale != null),
// boundaryMargin must be either fully infinite or fully finite, but not // boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both. // a mix of both.
assert( assert(
...@@ -143,6 +145,7 @@ class InteractiveViewer extends StatefulWidget { ...@@ -143,6 +145,7 @@ class InteractiveViewer extends StatefulWidget {
this.scaleFactor = 200.0, this.scaleFactor = 200.0,
this.transformationController, this.transformationController,
this.alignment, this.alignment,
this.trackpadScrollCausesScale = false,
required InteractiveViewerWidgetBuilder this.builder, required InteractiveViewerWidgetBuilder this.builder,
}) : assert(panAxis != null), }) : assert(panAxis != null),
assert(builder != null), assert(builder != null),
...@@ -156,6 +159,7 @@ class InteractiveViewer extends StatefulWidget { ...@@ -156,6 +159,7 @@ class InteractiveViewer extends StatefulWidget {
assert(maxScale >= minScale), assert(maxScale >= minScale),
assert(panEnabled != null), assert(panEnabled != null),
assert(scaleEnabled != null), assert(scaleEnabled != null),
assert(trackpadScrollCausesScale != null),
// boundaryMargin must be either fully infinite or fully finite, but not // boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both. // a mix of both.
assert( assert(
...@@ -295,10 +299,12 @@ class InteractiveViewer extends StatefulWidget { ...@@ -295,10 +299,12 @@ class InteractiveViewer extends StatefulWidget {
/// * [panEnabled], which is similar but for panning. /// * [panEnabled], which is similar but for panning.
final bool scaleEnabled; final bool scaleEnabled;
/// {@macro flutter.gestures.scale.trackpadScrollCausesScale}
final bool trackpadScrollCausesScale;
/// Determines the amount of scale to be performed per pointer scroll. /// Determines the amount of scale to be performed per pointer scroll.
/// ///
/// Defaults to 200.0, which was arbitrarily chosen to feel natural for most /// Defaults to [kDefaultMouseScrollToScaleFactor].
/// trackpads and mousewheels on all supported platforms.
/// ///
/// Increasing this value above the default causes scaling to feel slower, /// Increasing this value above the default causes scaling to feel slower,
/// while decreasing it causes scaling to feel faster. /// while decreasing it causes scaling to feel faster.
...@@ -556,7 +562,10 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -556,7 +562,10 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
final GlobalKey _childKey = GlobalKey(); final GlobalKey _childKey = GlobalKey();
final GlobalKey _parentKey = GlobalKey(); final GlobalKey _parentKey = GlobalKey();
Animation<Offset>? _animation; Animation<Offset>? _animation;
Animation<double>? _scaleAnimation;
late Offset _scaleAnimationFocalPoint;
late AnimationController _controller; late AnimationController _controller;
late AnimationController _scaleController;
Axis? _currentAxis; // Used with panAxis. Axis? _currentAxis; // Used with panAxis.
Offset? _referenceFocalPoint; // Point where the current gesture began. Offset? _referenceFocalPoint; // Point where the current gesture began.
double? _scaleStart; // Scale value at start of scaling gesture. double? _scaleStart; // Scale value at start of scaling gesture.
...@@ -795,6 +804,12 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -795,6 +804,12 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
_animation?.removeListener(_onAnimate); _animation?.removeListener(_onAnimate);
_animation = null; _animation = null;
} }
if (_scaleController.isAnimating) {
_scaleController.stop();
_scaleController.reset();
_scaleAnimation?.removeListener(_onScaleAnimate);
_scaleAnimation = null;
}
_gestureType = null; _gestureType = null;
_currentAxis = null; _currentAxis = null;
...@@ -809,6 +824,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -809,6 +824,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
// handled with GestureDetector's scale gesture. // handled with GestureDetector's scale gesture.
void _onScaleUpdate(ScaleUpdateDetails details) { void _onScaleUpdate(ScaleUpdateDetails details) {
final double scale = _transformationController!.value.getMaxScaleOnAxis(); final double scale = _transformationController!.value.getMaxScaleOnAxis();
_scaleAnimationFocalPoint = details.localFocalPoint;
final Offset focalPointScene = _transformationController!.toScene( final Offset focalPointScene = _transformationController!.toScene(
details.localFocalPoint, details.localFocalPoint,
); );
...@@ -913,45 +929,69 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -913,45 +929,69 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
_referenceFocalPoint = null; _referenceFocalPoint = null;
_animation?.removeListener(_onAnimate); _animation?.removeListener(_onAnimate);
_scaleAnimation?.removeListener(_onScaleAnimate);
_controller.reset(); _controller.reset();
_scaleController.reset();
if (!_gestureIsSupported(_gestureType)) { if (!_gestureIsSupported(_gestureType)) {
_currentAxis = null; _currentAxis = null;
return; return;
} }
// If the scale ended with enough velocity, animate inertial movement. if (_gestureType == _GestureType.pan) {
if (_gestureType != _GestureType.pan || details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
_currentAxis = null; _currentAxis = null;
return; return;
}
final Vector3 translationVector = _transformationController!.value.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
final FrictionSimulation frictionSimulationX = FrictionSimulation(
widget.interactionEndFrictionCoefficient,
translation.dx,
details.velocity.pixelsPerSecond.dx,
);
final FrictionSimulation frictionSimulationY = FrictionSimulation(
widget.interactionEndFrictionCoefficient,
translation.dy,
details.velocity.pixelsPerSecond.dy,
);
final double tFinal = _getFinalTime(
details.velocity.pixelsPerSecond.distance,
widget.interactionEndFrictionCoefficient,
);
_animation = Tween<Offset>(
begin: translation,
end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.decelerate,
));
_controller.duration = Duration(milliseconds: (tFinal * 1000).round());
_animation!.addListener(_onAnimate);
_controller.forward();
} else if (_gestureType == _GestureType.scale) {
if (details.scaleVelocity.abs() < 0.1) {
_currentAxis = null;
return;
}
final double scale = _transformationController!.value.getMaxScaleOnAxis();
final FrictionSimulation frictionSimulation = FrictionSimulation(
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
scale,
details.scaleVelocity / 10
);
final double tFinal = _getFinalTime(details.scaleVelocity.abs(), widget.interactionEndFrictionCoefficient, effectivelyMotionless: 0.1);
_scaleAnimation = Tween<double>(
begin: scale,
end: frictionSimulation.x(tFinal)
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.decelerate
));
_scaleController.duration = Duration(milliseconds: (tFinal * 1000).round());
_scaleAnimation!.addListener(_onScaleAnimate);
_scaleController.forward();
} }
final Vector3 translationVector = _transformationController!.value.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
final FrictionSimulation frictionSimulationX = FrictionSimulation(
widget.interactionEndFrictionCoefficient,
translation.dx,
details.velocity.pixelsPerSecond.dx,
);
final FrictionSimulation frictionSimulationY = FrictionSimulation(
widget.interactionEndFrictionCoefficient,
translation.dy,
details.velocity.pixelsPerSecond.dy,
);
final double tFinal = _getFinalTime(
details.velocity.pixelsPerSecond.distance,
widget.interactionEndFrictionCoefficient,
);
_animation = Tween<Offset>(
begin: translation,
end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.decelerate,
));
_controller.duration = Duration(milliseconds: (tFinal * 1000).round());
_animation!.addListener(_onAnimate);
_controller.forward();
} }
// Handle mousewheel and web trackpad scroll events. // Handle mousewheel and web trackpad scroll events.
...@@ -1085,6 +1125,38 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -1085,6 +1125,38 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
); );
} }
// Handle inertia scale animation.
void _onScaleAnimate() {
if (!_scaleController.isAnimating) {
_currentAxis = null;
_scaleAnimation?.removeListener(_onScaleAnimate);
_scaleAnimation = null;
_scaleController.reset();
return;
}
final double desiredScale = _scaleAnimation!.value;
final double scaleChange = desiredScale / _transformationController!.value.getMaxScaleOnAxis();
final Offset referenceFocalPoint = _transformationController!.toScene(
_scaleAnimationFocalPoint,
);
_transformationController!.value = _matrixScale(
_transformationController!.value,
scaleChange,
);
// While scaling, translate such that the user's two fingers stay on
// the same places in the scene. That means that the focal point of
// the scale should be on the same place in the scene before and after
// the scale.
final Offset focalPointSceneScaled = _transformationController!.toScene(
_scaleAnimationFocalPoint,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
focalPointSceneScaled - referenceFocalPoint,
);
}
void _onTransformationControllerChange() { void _onTransformationControllerChange() {
// A change to the TransformationController's value is a change to the // A change to the TransformationController's value is a change to the
// state. // state.
...@@ -1101,6 +1173,9 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -1101,6 +1173,9 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
); );
_scaleController = AnimationController(
vsync: this
);
} }
@override @override
...@@ -1131,6 +1206,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -1131,6 +1206,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
_scaleController.dispose();
_transformationController!.removeListener(_onTransformationControllerChange); _transformationController!.removeListener(_onTransformationControllerChange);
if (widget.transformationController == null) { if (widget.transformationController == null) {
_transformationController!.dispose(); _transformationController!.dispose();
...@@ -1181,6 +1257,8 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -1181,6 +1257,8 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
onScaleEnd: _onScaleEnd, onScaleEnd: _onScaleEnd,
onScaleStart: _onScaleStart, onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate, onScaleUpdate: _onScaleUpdate,
trackpadScrollCausesScale: widget.trackpadScrollCausesScale,
trackpadScrollToScaleFactor: Offset(0, -1/widget.scaleFactor),
child: child, child: child,
), ),
); );
...@@ -1305,8 +1383,7 @@ enum _GestureType { ...@@ -1305,8 +1383,7 @@ enum _GestureType {
// Given a velocity and drag, calculate the time at which motion will come to // Given a velocity and drag, calculate the time at which motion will come to
// a stop, within the margin of effectivelyMotionless. // a stop, within the margin of effectivelyMotionless.
double _getFinalTime(double velocity, double drag) { double _getFinalTime(double velocity, double drag, {double effectivelyMotionless = 10}) {
const double effectivelyMotionless = 10.0;
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
} }
......
...@@ -1173,4 +1173,196 @@ void main() { ...@@ -1173,4 +1173,196 @@ void main() {
), ),
); );
}); });
testGesture('scale trackpadScrollCausesScale', (GestureTester tester) {
final ScaleGestureRecognizer scale = ScaleGestureRecognizer(
dragStartBehavior: DragStartBehavior.start,
trackpadScrollCausesScale: true
);
bool didStartScale = false;
Offset? updatedFocalPoint;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
};
double? updatedScale;
Offset? updatedDelta;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedFocalPoint = details.focalPoint;
updatedDelta = details.focalPointDelta;
};
bool didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
final TestPointer pointer1 = TestPointer(2, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero);
scale.addPointerPanZoom(start);
tester.closeArena(2);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isFalse);
tester.route(start);
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedDelta, isNull);
expect(didEndScale, isFalse);
// Zoom in by scrolling up.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(0, -200)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, math.e);
updatedScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(didEndScale, isFalse);
// A horizontal scroll should do nothing.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(200, -200)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, math.e);
updatedScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(didEndScale, isFalse);
// End.
tester.route(pointer1.panZoomEnd());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
// Try with a different trackpadScrollToScaleFactor
scale.trackpadScrollToScaleFactor = const Offset(1/125, 0);
final PointerPanZoomStartEvent start2 = pointer1.panZoomStart(Offset.zero);
scale.addPointerPanZoom(start2);
tester.closeArena(2);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isFalse);
tester.route(start2);
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedDelta, isNull);
expect(didEndScale, isFalse);
// Zoom in by scrolling left.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(125, 0)));
expect(didStartScale, isFalse);
didStartScale = false;
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, math.e);
updatedScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(didEndScale, isFalse);
// A vertical scroll should do nothing.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(125, 125)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, math.e);
updatedScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(didEndScale, isFalse);
// End.
tester.route(pointer1.panZoomEnd());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
scale.dispose();
});
testGesture('scale ending velocity', (GestureTester tester) {
final ScaleGestureRecognizer scale = ScaleGestureRecognizer(
dragStartBehavior: DragStartBehavior.start,
trackpadScrollCausesScale: true
);
bool didStartScale = false;
Offset? updatedFocalPoint;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
};
bool didEndScale = false;
double? scaleEndVelocity;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
scaleEndVelocity = details.scaleVelocity;
};
final TestPointer pointer1 = TestPointer(2, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero);
scale.addPointerPanZoom(start);
tester.closeArena(2);
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(didEndScale, isFalse);
tester.route(start);
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(didEndScale, isFalse);
// Zoom in by scrolling up.
for (int i = 0; i < 100; i++) {
tester.route(pointer1.panZoomUpdate(
Offset.zero,
pan: Offset(0, i * -10),
timeStamp: Duration(milliseconds: i * 25)
));
}
// End.
tester.route(pointer1.panZoomEnd(timeStamp: const Duration(milliseconds: 2500)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
expect(scaleEndVelocity, moreOrLessEquals(281.41454098027765));
scale.dispose();
});
} }
...@@ -1802,6 +1802,78 @@ void main() { ...@@ -1802,6 +1802,78 @@ void main() {
await tester.pump(); await tester.pump();
expect(transformationController.value.getMaxScaleOnAxis(), 2.5); // capped at maxScale (2.5) expect(transformationController.value.getMaxScaleOnAxis(), 2.5); // capped at maxScale (2.5)
}); });
testWidgets('trackpadScrollCausesScale', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
const double boundaryMargin = 50.0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
boundaryMargin: const EdgeInsets.all(boundaryMargin),
transformationController: transformationController,
trackpadScrollCausesScale: true,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value.getMaxScaleOnAxis(), 1.0);
// Send a vertical scroll.
final TestPointer pointer = TestPointer(1, PointerDeviceKind.trackpad);
final Offset center = tester.getCenter(find.byType(SizedBox));
await tester.sendEventToBinding(pointer.panZoomStart(center));
await tester.pump();
expect(transformationController.value.getMaxScaleOnAxis(), 1.0);
await tester.sendEventToBinding(pointer.panZoomUpdate(center, pan: const Offset(0, -81)));
await tester.pump();
expect(transformationController.value.getMaxScaleOnAxis(), moreOrLessEquals(1.499302500056767));
// Send a horizontal scroll (should have no effect).
await tester.sendEventToBinding(pointer.panZoomUpdate(center, pan: const Offset(81, -81)));
await tester.pump();
expect(transformationController.value.getMaxScaleOnAxis(), moreOrLessEquals(1.499302500056767));
});
testWidgets('Scaling inertia', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
const double boundaryMargin = 50.0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
boundaryMargin: const EdgeInsets.all(boundaryMargin),
transformationController: transformationController,
trackpadScrollCausesScale: true,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value.getMaxScaleOnAxis(), 1.0);
// Send a vertical scroll fling, which will cause inertia.
await tester.trackpadFling(
find.byType(InteractiveViewer),
const Offset(0, -100),
3000
);
await tester.pump();
expect(transformationController.value.getMaxScaleOnAxis(), moreOrLessEquals(1.6487212707001282));
await tester.pump(const Duration(milliseconds: 80));
expect(transformationController.value.getMaxScaleOnAxis(), moreOrLessEquals(1.7966838346780103));
await tester.pumpAndSettle();
expect(transformationController.value.getMaxScaleOnAxis(), moreOrLessEquals(1.9984509673751225));
await tester.pump(const Duration(seconds: 10));
expect(transformationController.value.getMaxScaleOnAxis(), moreOrLessEquals(1.9984509673751225));
});
}); });
group('getNearestPointOnLine', () { group('getNearestPointOnLine', () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment