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 '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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
/// 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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
_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 {
|
|||||||
_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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
_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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
..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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// * [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
|
|||||||
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
|
|||||||
_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
|
|||||||
// 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
|
|||||||
_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
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
|||||||
_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
|
|||||||
@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
|
|||||||
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 {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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();
|
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', () {
|
||||||
|
Loading…
Reference in New Issue
Block a user