Unverified Commit 4aea2f99 authored by Frank Hinkel's avatar Frank Hinkel Committed by GitHub

Add friction coefficient to InteractiveViewer (#109443)

parent 8b36f4fb
...@@ -79,6 +79,7 @@ class InteractiveViewer extends StatefulWidget { ...@@ -79,6 +79,7 @@ class InteractiveViewer extends StatefulWidget {
// use cases. // use cases.
this.maxScale = 2.5, this.maxScale = 2.5,
this.minScale = 0.8, this.minScale = 0.8,
this.interactionEndFrictionCoefficient = _kDrag,
this.onInteractionEnd, this.onInteractionEnd,
this.onInteractionStart, this.onInteractionStart,
this.onInteractionUpdate, this.onInteractionUpdate,
...@@ -93,6 +94,7 @@ class InteractiveViewer extends StatefulWidget { ...@@ -93,6 +94,7 @@ class InteractiveViewer extends StatefulWidget {
assert(constrained != null), assert(constrained != null),
assert(minScale != null), assert(minScale != null),
assert(minScale > 0), assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite), assert(minScale.isFinite),
assert(maxScale != null), assert(maxScale != null),
assert(maxScale > 0), assert(maxScale > 0),
...@@ -131,6 +133,7 @@ class InteractiveViewer extends StatefulWidget { ...@@ -131,6 +133,7 @@ class InteractiveViewer extends StatefulWidget {
// use cases. // use cases.
this.maxScale = 2.5, this.maxScale = 2.5,
this.minScale = 0.8, this.minScale = 0.8,
this.interactionEndFrictionCoefficient = _kDrag,
this.onInteractionEnd, this.onInteractionEnd,
this.onInteractionStart, this.onInteractionStart,
this.onInteractionUpdate, this.onInteractionUpdate,
...@@ -143,6 +146,7 @@ class InteractiveViewer extends StatefulWidget { ...@@ -143,6 +146,7 @@ class InteractiveViewer extends StatefulWidget {
assert(builder != null), assert(builder != null),
assert(minScale != null), assert(minScale != null),
assert(minScale > 0), assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite), assert(minScale.isFinite),
assert(maxScale != null), assert(maxScale != null),
assert(maxScale > 0), assert(maxScale > 0),
...@@ -326,6 +330,13 @@ class InteractiveViewer extends StatefulWidget { ...@@ -326,6 +330,13 @@ class InteractiveViewer extends StatefulWidget {
/// than maxScale. /// than maxScale.
final double minScale; final double minScale;
/// Changes the deceleration behavior after a gesture.
///
/// Defaults to 0.0000135.
///
/// Cannot be null, and must be a finite number greater than zero.
final double interactionEndFrictionCoefficient;
/// Called when the user ends a pan or scale gesture on the widget. /// Called when the user ends a pan or scale gesture on the widget.
/// ///
/// At the time this is called, the [TransformationController] will have /// At the time this is called, the [TransformationController] will have
...@@ -408,6 +419,10 @@ class InteractiveViewer extends StatefulWidget { ...@@ -408,6 +419,10 @@ class InteractiveViewer extends StatefulWidget {
/// * [TextEditingController] for an example of another similar pattern. /// * [TextEditingController] for an example of another similar pattern.
final TransformationController? transformationController; final TransformationController? transformationController;
// Used as the coefficient of friction in the inertial translation animation.
// This value was eyeballed to give a feel similar to Google Photos.
static const double _kDrag = 0.0000135;
/// Returns the closest point to the given point on the given line segment. /// Returns the closest point to the given point on the given line segment.
@visibleForTesting @visibleForTesting
static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) { static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) {
...@@ -549,10 +564,6 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -549,10 +564,6 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
// https://github.com/flutter/flutter/issues/57698 // https://github.com/flutter/flutter/issues/57698
final bool _rotateEnabled = false; final bool _rotateEnabled = false;
// Used as the coefficient of friction in the inertial translation animation.
// This value was eyeballed to give a feel similar to Google Photos.
static const double _kDrag = 0.0000135;
// The _boundaryRect is calculated by adding the boundaryMargin to the size of // The _boundaryRect is calculated by adding the boundaryMargin to the size of
// the child. // the child.
Rect get _boundaryRect { Rect get _boundaryRect {
...@@ -913,18 +924,18 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -913,18 +924,18 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
final Vector3 translationVector = _transformationController!.value.getTranslation(); final Vector3 translationVector = _transformationController!.value.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y); final Offset translation = Offset(translationVector.x, translationVector.y);
final FrictionSimulation frictionSimulationX = FrictionSimulation( final FrictionSimulation frictionSimulationX = FrictionSimulation(
_kDrag, widget.interactionEndFrictionCoefficient,
translation.dx, translation.dx,
details.velocity.pixelsPerSecond.dx, details.velocity.pixelsPerSecond.dx,
); );
final FrictionSimulation frictionSimulationY = FrictionSimulation( final FrictionSimulation frictionSimulationY = FrictionSimulation(
_kDrag, widget.interactionEndFrictionCoefficient,
translation.dy, translation.dy,
details.velocity.pixelsPerSecond.dy, details.velocity.pixelsPerSecond.dy,
); );
final double tFinal = _getFinalTime( final double tFinal = _getFinalTime(
details.velocity.pixelsPerSecond.distance, details.velocity.pixelsPerSecond.distance,
_kDrag, widget.interactionEndFrictionCoefficient,
); );
_animation = Tween<Offset>( _animation = Tween<Offset>(
begin: translation, begin: translation,
......
...@@ -1649,6 +1649,62 @@ void main() { ...@@ -1649,6 +1649,62 @@ void main() {
expect(scaleHighZoomedIn, greaterThan(scaleHighZoomedOut)); expect(scaleHighZoomedIn, greaterThan(scaleHighZoomedOut));
expect(scaleHighZoomedIn - scaleHighZoomedOut, lessThan(scaleZoomedIn - scaleZoomedOut)); expect(scaleHighZoomedIn - scaleHighZoomedOut, lessThan(scaleZoomedIn - scaleZoomedOut));
}); });
testWidgets('interactionEndFrictionCoefficient', (WidgetTester tester) async {
// Use the default interactionEndFrictionCoefficient.
final TransformationController transformationController1 = TransformationController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 200,
height: 200,
child: InteractiveViewer(
constrained: false,
transformationController: transformationController1,
child: const SizedBox(width: 2000.0, height: 2000.0),
),
),
),
),
);
expect(transformationController1.value, equals(Matrix4.identity()));
await tester.flingFrom(const Offset(100, 100), const Offset(0, -50), 100.0);
await tester.pumpAndSettle();
final Vector3 translation1 = transformationController1.value.getTranslation();
expect(translation1.y, lessThan(-58.0));
// Next try a custom interactionEndFrictionCoefficient.
final TransformationController transformationController2 = TransformationController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 200,
height: 200,
child: InteractiveViewer(
constrained: false,
interactionEndFrictionCoefficient: 0.01,
transformationController: transformationController2,
child: const SizedBox(width: 2000.0, height: 2000.0),
),
),
),
),
);
expect(transformationController2.value, equals(Matrix4.identity()));
await tester.flingFrom(const Offset(100, 100), const Offset(0, -50), 100.0);
await tester.pumpAndSettle();
final Vector3 translation2 = transformationController2.value.getTranslation();
// The coefficient 0.01 is greater than the default of 0.0000135,
// so the translation comes to a stop more quickly.
expect(translation2.y, lessThan(translation1.y));
});
}); });
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