mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
InteractiveViewer parameter to return to pre-3.3 trackpad/Magic Mouse behaviour (#114280)
* trackpadPanShouldActAsZoom * Address feedback * Move constant, add blank lines
This commit is contained in:
parent
71f920732b
commit
7ddf42eae5
@ -15,6 +15,18 @@ export 'events.dart' show PointerDownEvent, PointerEvent, PointerPanZoomStartEve
|
||||
export 'recognizer.dart' show DragStartBehavior;
|
||||
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].
|
||||
enum _ScaleState {
|
||||
@ -36,17 +48,49 @@ enum _ScaleState {
|
||||
}
|
||||
|
||||
class _PointerPanZoomData {
|
||||
_PointerPanZoomData({
|
||||
required this.focalPoint,
|
||||
required this.scale,
|
||||
required this.rotation
|
||||
});
|
||||
Offset focalPoint;
|
||||
double scale;
|
||||
double rotation;
|
||||
_PointerPanZoomData.fromStartEvent(
|
||||
this.parent,
|
||||
PointerPanZoomStartEvent event
|
||||
) : _position = event.position,
|
||||
_pan = Offset.zero,
|
||||
_scale = 1,
|
||||
_rotation = 0;
|
||||
|
||||
_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
|
||||
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].
|
||||
@ -54,8 +98,11 @@ class ScaleStartDetails {
|
||||
/// Creates details for [GestureScaleStartCallback].
|
||||
///
|
||||
/// The [focalPoint] argument must not be null.
|
||||
ScaleStartDetails({ this.focalPoint = Offset.zero, Offset? localFocalPoint, this.pointerCount = 0 })
|
||||
: assert(focalPoint != null), localFocalPoint = localFocalPoint ?? focalPoint;
|
||||
ScaleStartDetails({
|
||||
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.
|
||||
///
|
||||
@ -201,12 +248,15 @@ class ScaleEndDetails {
|
||||
/// Creates details for [GestureScaleEndCallback].
|
||||
///
|
||||
/// 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);
|
||||
|
||||
/// The velocity of the last pointer to be lifted off of the screen.
|
||||
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.
|
||||
///
|
||||
/// Typically this is the number of fingers being used to pan the widget using the gesture
|
||||
@ -214,7 +264,7 @@ class ScaleEndDetails {
|
||||
final int pointerCount;
|
||||
|
||||
@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
|
||||
@ -285,6 +335,8 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
this.dragStartBehavior = DragStartBehavior.down,
|
||||
this.trackpadScrollCausesScale = false,
|
||||
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
|
||||
}) : assert(dragStartBehavior != null);
|
||||
|
||||
/// Determines what point is used as the starting point in all calculations
|
||||
@ -332,6 +384,26 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
|
||||
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;
|
||||
Offset? _currentFocalPoint;
|
||||
late double _initialSpan;
|
||||
@ -346,6 +418,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
final Map<int, Offset> _pointerLocations = <int, Offset>{};
|
||||
final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance
|
||||
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
||||
VelocityTracker? _scaleVelocityTracker;
|
||||
late Offset _delta;
|
||||
final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
|
||||
double _initialPanZoomScaleFactor = 1;
|
||||
@ -466,23 +539,16 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
_lastTransform = event.transform;
|
||||
} else if (event is PointerPanZoomStartEvent) {
|
||||
assert(_pointerPanZooms[event.pointer] == null);
|
||||
_pointerPanZooms[event.pointer] = _PointerPanZoomData(
|
||||
focalPoint: event.position,
|
||||
scale: 1,
|
||||
rotation: 0
|
||||
);
|
||||
_pointerPanZooms[event.pointer] = _PointerPanZoomData.fromStartEvent(this, event);
|
||||
didChangeConfiguration = true;
|
||||
shouldStartIfAccepted = true;
|
||||
_lastTransform = event.transform;
|
||||
} else if (event is PointerPanZoomUpdateEvent) {
|
||||
assert(_pointerPanZooms[event.pointer] != null);
|
||||
if (!event.synthesized) {
|
||||
if (!event.synthesized && !trackpadScrollCausesScale) {
|
||||
_velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
|
||||
}
|
||||
_pointerPanZooms[event.pointer] = _PointerPanZoomData(
|
||||
focalPoint: event.position + event.pan,
|
||||
scale: event.scale,
|
||||
rotation: event.rotation
|
||||
);
|
||||
_pointerPanZooms[event.pointer] = _PointerPanZoomData.fromUpdateEvent(this, event);
|
||||
_lastTransform = event.transform;
|
||||
shouldStartIfAccepted = true;
|
||||
} else if (event is PointerPanZoomEndEvent) {
|
||||
@ -495,7 +561,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
_update();
|
||||
|
||||
if (!didChangeConfiguration || _reconfigure(event.pointer)) {
|
||||
_advanceStateMachine(shouldStartIfAccepted, event.kind);
|
||||
_advanceStateMachine(shouldStartIfAccepted, event);
|
||||
}
|
||||
stopTrackingIfPointerNoLongerDown(event);
|
||||
}
|
||||
@ -607,18 +673,20 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * 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 {
|
||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount)));
|
||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
|
||||
}
|
||||
}
|
||||
_state = _ScaleState.accepted;
|
||||
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
|
||||
return false;
|
||||
}
|
||||
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
|
||||
return true;
|
||||
}
|
||||
|
||||
void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
|
||||
void _advanceStateMachine(bool shouldStartIfAccepted, PointerEvent event) {
|
||||
if (_state == _ScaleState.ready) {
|
||||
_state = _ScaleState.possible;
|
||||
}
|
||||
@ -626,7 +694,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
if (_state == _ScaleState.possible) {
|
||||
final double spanDelta = (_currentSpan - _initialSpan).abs();
|
||||
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);
|
||||
}
|
||||
} else if (_state.index >= _ScaleState.accepted.index) {
|
||||
@ -638,19 +706,22 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
_dispatchOnStartCallbackIfNeeded();
|
||||
}
|
||||
|
||||
if (_state == _ScaleState.started && onUpdate != null) {
|
||||
invokeCallback<void>('onUpdate', () {
|
||||
onUpdate!(ScaleUpdateDetails(
|
||||
scale: _scaleFactor,
|
||||
horizontalScale: _horizontalScaleFactor,
|
||||
verticalScale: _verticalScaleFactor,
|
||||
focalPoint: _currentFocalPoint!,
|
||||
localFocalPoint: _localFocalPoint,
|
||||
rotation: _computeRotationFactor(),
|
||||
pointerCount: _pointerCount,
|
||||
focalPointDelta: _delta,
|
||||
));
|
||||
});
|
||||
if (_state == _ScaleState.started) {
|
||||
_scaleVelocityTracker?.addPosition(event.timeStamp, Offset(_scaleFactor, 0));
|
||||
if (onUpdate != null) {
|
||||
invokeCallback<void>('onUpdate', () {
|
||||
onUpdate!(ScaleUpdateDetails(
|
||||
scale: _scaleFactor,
|
||||
horizontalScale: _horizontalScaleFactor,
|
||||
verticalScale: _verticalScaleFactor,
|
||||
focalPoint: _currentFocalPoint!,
|
||||
localFocalPoint: _localFocalPoint,
|
||||
rotation: _computeRotationFactor(),
|
||||
pointerCount: _pointerCount,
|
||||
focalPointDelta: _delta,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,6 +288,8 @@ class GestureDetector extends StatelessWidget {
|
||||
this.behavior,
|
||||
this.excludeFromSemantics = false,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.trackpadScrollCausesScale = false,
|
||||
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
|
||||
this.supportedDevices,
|
||||
}) : assert(excludeFromSemantics != null),
|
||||
assert(dragStartBehavior != null),
|
||||
@ -1014,6 +1016,12 @@ class GestureDetector extends StatelessWidget {
|
||||
/// If set to null, events from all device types will be recognized. Defaults to null.
|
||||
final Set<PointerDeviceKind>? supportedDevices;
|
||||
|
||||
/// {@macro flutter.gestures.scale.trackpadScrollCausesScale}
|
||||
final bool trackpadScrollCausesScale;
|
||||
|
||||
/// {@macro flutter.gestures.scale.trackpadScrollToScaleFactor}
|
||||
final Offset trackpadScrollToScaleFactor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
|
||||
@ -1186,7 +1194,9 @@ class GestureDetector extends StatelessWidget {
|
||||
..onUpdate = onScaleUpdate
|
||||
..onEnd = onScaleEnd
|
||||
..dragStartBehavior = dragStartBehavior
|
||||
..gestureSettings = gestureSettings;
|
||||
..gestureSettings = gestureSettings
|
||||
..trackpadScrollCausesScale = trackpadScrollCausesScale
|
||||
..trackpadScrollToScaleFactor = trackpadScrollToScaleFactor;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -85,9 +85,10 @@ class InteractiveViewer extends StatefulWidget {
|
||||
this.onInteractionUpdate,
|
||||
this.panEnabled = true,
|
||||
this.scaleEnabled = true,
|
||||
this.scaleFactor = 200.0,
|
||||
this.scaleFactor = kDefaultMouseScrollToScaleFactor,
|
||||
this.transformationController,
|
||||
this.alignment,
|
||||
this.trackpadScrollCausesScale = false,
|
||||
required Widget this.child,
|
||||
}) : assert(alignPanAxis != null),
|
||||
assert(panAxis != null),
|
||||
@ -103,6 +104,7 @@ class InteractiveViewer extends StatefulWidget {
|
||||
assert(maxScale >= minScale),
|
||||
assert(panEnabled != null),
|
||||
assert(scaleEnabled != null),
|
||||
assert(trackpadScrollCausesScale != null),
|
||||
// boundaryMargin must be either fully infinite or fully finite, but not
|
||||
// a mix of both.
|
||||
assert(
|
||||
@ -143,6 +145,7 @@ class InteractiveViewer extends StatefulWidget {
|
||||
this.scaleFactor = 200.0,
|
||||
this.transformationController,
|
||||
this.alignment,
|
||||
this.trackpadScrollCausesScale = false,
|
||||
required InteractiveViewerWidgetBuilder this.builder,
|
||||
}) : assert(panAxis != null),
|
||||
assert(builder != null),
|
||||
@ -156,6 +159,7 @@ class InteractiveViewer extends StatefulWidget {
|
||||
assert(maxScale >= minScale),
|
||||
assert(panEnabled != null),
|
||||
assert(scaleEnabled != null),
|
||||
assert(trackpadScrollCausesScale != null),
|
||||
// boundaryMargin must be either fully infinite or fully finite, but not
|
||||
// a mix of both.
|
||||
assert(
|
||||
@ -295,10 +299,12 @@ class InteractiveViewer extends StatefulWidget {
|
||||
/// * [panEnabled], which is similar but for panning.
|
||||
final bool scaleEnabled;
|
||||
|
||||
/// {@macro flutter.gestures.scale.trackpadScrollCausesScale}
|
||||
final bool trackpadScrollCausesScale;
|
||||
|
||||
/// Determines the amount of scale to be performed per pointer scroll.
|
||||
///
|
||||
/// Defaults to 200.0, which was arbitrarily chosen to feel natural for most
|
||||
/// trackpads and mousewheels on all supported platforms.
|
||||
/// Defaults to [kDefaultMouseScrollToScaleFactor].
|
||||
///
|
||||
/// Increasing this value above the default causes scaling to feel slower,
|
||||
/// while decreasing it causes scaling to feel faster.
|
||||
@ -556,7 +562,10 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
|
||||
final GlobalKey _childKey = GlobalKey();
|
||||
final GlobalKey _parentKey = GlobalKey();
|
||||
Animation<Offset>? _animation;
|
||||
Animation<double>? _scaleAnimation;
|
||||
late Offset _scaleAnimationFocalPoint;
|
||||
late AnimationController _controller;
|
||||
late AnimationController _scaleController;
|
||||
Axis? _currentAxis; // Used with panAxis.
|
||||
Offset? _referenceFocalPoint; // Point where the current gesture began.
|
||||
double? _scaleStart; // Scale value at start of scaling gesture.
|
||||
@ -795,6 +804,12 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
|
||||
_animation?.removeListener(_onAnimate);
|
||||
_animation = null;
|
||||
}
|
||||
if (_scaleController.isAnimating) {
|
||||
_scaleController.stop();
|
||||
_scaleController.reset();
|
||||
_scaleAnimation?.removeListener(_onScaleAnimate);
|
||||
_scaleAnimation = null;
|
||||
}
|
||||
|
||||
_gestureType = null;
|
||||
_currentAxis = null;
|
||||
@ -809,6 +824,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
|
||||
// handled with GestureDetector's scale gesture.
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
final double scale = _transformationController!.value.getMaxScaleOnAxis();
|
||||
_scaleAnimationFocalPoint = details.localFocalPoint;
|
||||
final Offset focalPointScene = _transformationController!.toScene(
|
||||
details.localFocalPoint,
|
||||
);
|
||||
@ -913,45 +929,69 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
|
||||
_referenceFocalPoint = null;
|
||||
|
||||
_animation?.removeListener(_onAnimate);
|
||||
_scaleAnimation?.removeListener(_onScaleAnimate);
|
||||
_controller.reset();
|
||||
_scaleController.reset();
|
||||
|
||||
if (!_gestureIsSupported(_gestureType)) {
|
||||
_currentAxis = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the scale ended with enough velocity, animate inertial movement.
|
||||
if (_gestureType != _GestureType.pan || details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
|
||||
_currentAxis = null;
|
||||
return;
|
||||
if (_gestureType == _GestureType.pan) {
|
||||
if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
|
||||
_currentAxis = null;
|
||||
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.
|
||||
@ -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() {
|
||||
// A change to the TransformationController's value is a change to the
|
||||
// state.
|
||||
@ -1101,6 +1173,9 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
);
|
||||
_scaleController = AnimationController(
|
||||
vsync: this
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1131,6 +1206,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_scaleController.dispose();
|
||||
_transformationController!.removeListener(_onTransformationControllerChange);
|
||||
if (widget.transformationController == null) {
|
||||
_transformationController!.dispose();
|
||||
@ -1181,6 +1257,8 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
|
||||
onScaleEnd: _onScaleEnd,
|
||||
onScaleStart: _onScaleStart,
|
||||
onScaleUpdate: _onScaleUpdate,
|
||||
trackpadScrollCausesScale: widget.trackpadScrollCausesScale,
|
||||
trackpadScrollToScaleFactor: Offset(0, -1/widget.scaleFactor),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
@ -1305,8 +1383,7 @@ enum _GestureType {
|
||||
|
||||
// Given a velocity and drag, calculate the time at which motion will come to
|
||||
// a stop, within the margin of effectivelyMotionless.
|
||||
double _getFinalTime(double velocity, double drag) {
|
||||
const double effectivelyMotionless = 10.0;
|
||||
double _getFinalTime(double velocity, double drag, {double effectivelyMotionless = 10}) {
|
||||
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
await tester.pump();
|
||||
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', () {
|
||||
|
Loading…
Reference in New Issue
Block a user