Unverified Commit 0ad0a56e authored by Nazareno Cavazzon's avatar Nazareno Cavazzon Committed by GitHub

panningDirection parameter added to InteractiveViewer (#109014)

parent 8074811c
...@@ -34,7 +34,7 @@ class MyStatelessWidget extends StatelessWidget { ...@@ -34,7 +34,7 @@ class MyStatelessWidget extends StatelessWidget {
const int columnCount = 6; const int columnCount = 6;
return InteractiveViewer( return InteractiveViewer(
alignPanAxis: true, panAxis: PanAxis.aligned,
constrained: false, constrained: false,
scaleEnabled: false, scaleEnabled: false,
child: Table( child: Table(
......
...@@ -67,7 +67,12 @@ class InteractiveViewer extends StatefulWidget { ...@@ -67,7 +67,12 @@ class InteractiveViewer extends StatefulWidget {
InteractiveViewer({ InteractiveViewer({
super.key, super.key,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
@Deprecated(
'Use panAxis instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
this.alignPanAxis = false, this.alignPanAxis = false,
this.panAxis = PanAxis.free,
this.boundaryMargin = EdgeInsets.zero, this.boundaryMargin = EdgeInsets.zero,
this.constrained = true, this.constrained = true,
// These default scale values were eyeballed as reasonable limits for common // These default scale values were eyeballed as reasonable limits for common
...@@ -83,6 +88,7 @@ class InteractiveViewer extends StatefulWidget { ...@@ -83,6 +88,7 @@ class InteractiveViewer extends StatefulWidget {
this.transformationController, this.transformationController,
required Widget this.child, required Widget this.child,
}) : assert(alignPanAxis != null), }) : assert(alignPanAxis != null),
assert(panAxis != null),
assert(child != null), assert(child != null),
assert(constrained != null), assert(constrained != null),
assert(minScale != null), assert(minScale != null),
...@@ -114,7 +120,12 @@ class InteractiveViewer extends StatefulWidget { ...@@ -114,7 +120,12 @@ class InteractiveViewer extends StatefulWidget {
InteractiveViewer.builder({ InteractiveViewer.builder({
super.key, super.key,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
@Deprecated(
'Use panAxis instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
this.alignPanAxis = false, this.alignPanAxis = false,
this.panAxis = PanAxis.free,
this.boundaryMargin = EdgeInsets.zero, this.boundaryMargin = EdgeInsets.zero,
// These default scale values were eyeballed as reasonable limits for common // These default scale values were eyeballed as reasonable limits for common
// use cases. // use cases.
...@@ -128,7 +139,7 @@ class InteractiveViewer extends StatefulWidget { ...@@ -128,7 +139,7 @@ class InteractiveViewer extends StatefulWidget {
this.scaleFactor = 200.0, this.scaleFactor = 200.0,
this.transformationController, this.transformationController,
required InteractiveViewerWidgetBuilder this.builder, required InteractiveViewerWidgetBuilder this.builder,
}) : assert(alignPanAxis != null), }) : assert(panAxis != null),
assert(builder != null), assert(builder != null),
assert(minScale != null), assert(minScale != null),
assert(minScale > 0), assert(minScale > 0),
...@@ -158,6 +169,8 @@ class InteractiveViewer extends StatefulWidget { ...@@ -158,6 +169,8 @@ class InteractiveViewer extends StatefulWidget {
/// Defaults to [Clip.hardEdge]. /// Defaults to [Clip.hardEdge].
final Clip clipBehavior; final Clip clipBehavior;
/// This property is deprecated, please use [panAxis] instead.
///
/// If true, panning is only allowed in the direction of the horizontal axis /// If true, panning is only allowed in the direction of the horizontal axis
/// or the vertical axis. /// or the vertical axis.
/// ///
...@@ -169,8 +182,25 @@ class InteractiveViewer extends StatefulWidget { ...@@ -169,8 +182,25 @@ class InteractiveViewer extends StatefulWidget {
/// See also: /// See also:
/// * [constrained], which has an example of creating a table that uses /// * [constrained], which has an example of creating a table that uses
/// alignPanAxis. /// alignPanAxis.
@Deprecated(
'Use panAxis instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
final bool alignPanAxis; final bool alignPanAxis;
/// When set to [PanAxis.aligned], panning is only allowed in the horizontal
/// axis or the vertical axis, diagonal panning is not allowed.
///
/// When set to [PanAxis.vertical] or [PanAxis.horizontal] panning is only
/// allowed in the specified axis. For example, if set to [PanAxis.vertical],
/// panning will only be allowed in the vertical axis. And if set to [PanAxis.horizontal],
/// panning will only be allowed in the horizontal axis.
///
/// When set to [PanAxis.free] panning is allowed in all directions.
///
/// Defaults to [PanAxis.free].
final PanAxis panAxis;
/// A margin for the visible boundaries of the child. /// A margin for the visible boundaries of the child.
/// ///
/// Any transformation that results in the viewport being able to view outside /// Any transformation that results in the viewport being able to view outside
...@@ -507,7 +537,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -507,7 +537,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
final GlobalKey _parentKey = GlobalKey(); final GlobalKey _parentKey = GlobalKey();
Animation<Offset>? _animation; Animation<Offset>? _animation;
late AnimationController _controller; late AnimationController _controller;
Axis? _panAxis; // Used with alignPanAxis. 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.
double? _rotationStart = 0.0; // Rotation at start of rotation gesture. double? _rotationStart = 0.0; // Rotation at start of rotation gesture.
...@@ -566,9 +596,26 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -566,9 +596,26 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
return matrix.clone(); return matrix.clone();
} }
final Offset alignedTranslation = widget.alignPanAxis && _panAxis != null late final Offset alignedTranslation;
? _alignAxis(translation, _panAxis!)
: translation; if (_currentAxis != null) {
switch(widget.panAxis){
case PanAxis.horizontal:
alignedTranslation = _alignAxis(translation, Axis.horizontal);
break;
case PanAxis.vertical:
alignedTranslation = _alignAxis(translation, Axis.vertical);
break;
case PanAxis.aligned:
alignedTranslation = _alignAxis(translation, _currentAxis!);
break;
case PanAxis.free:
alignedTranslation = translation;
break;
}
} else {
alignedTranslation = translation;
}
final Matrix4 nextMatrix = matrix.clone()..translate( final Matrix4 nextMatrix = matrix.clone()..translate(
alignedTranslation.dx, alignedTranslation.dx,
...@@ -734,7 +781,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -734,7 +781,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
} }
_gestureType = null; _gestureType = null;
_panAxis = null; _currentAxis = null;
_scaleStart = _transformationController!.value.getMaxScaleOnAxis(); _scaleStart = _transformationController!.value.getMaxScaleOnAxis();
_referenceFocalPoint = _transformationController!.toScene( _referenceFocalPoint = _transformationController!.toScene(
details.localFocalPoint, details.localFocalPoint,
...@@ -825,7 +872,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -825,7 +872,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
widget.onInteractionUpdate?.call(details); widget.onInteractionUpdate?.call(details);
return; return;
} }
_panAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene); _currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene);
// Translate so that the same point in the scene is underneath the // Translate so that the same point in the scene is underneath the
// focal point before and after the movement. // focal point before and after the movement.
final Offset translationChange = focalPointScene - _referenceFocalPoint!; final Offset translationChange = focalPointScene - _referenceFocalPoint!;
...@@ -853,13 +900,13 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -853,13 +900,13 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
_controller.reset(); _controller.reset();
if (!_gestureIsSupported(_gestureType)) { if (!_gestureIsSupported(_gestureType)) {
_panAxis = null; _currentAxis = null;
return; return;
} }
// If the scale ended with enough velocity, animate inertial movement. // If the scale ended with enough velocity, animate inertial movement.
if (_gestureType != _GestureType.pan || details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { if (_gestureType != _GestureType.pan || details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
_panAxis = null; _currentAxis = null;
return; return;
} }
...@@ -947,7 +994,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid ...@@ -947,7 +994,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
// Handle inertia drag animation. // Handle inertia drag animation.
void _onAnimate() { void _onAnimate() {
if (!_controller.isAnimating) { if (!_controller.isAnimating) {
_panAxis = null; _currentAxis = null;
_animation?.removeListener(_onAnimate); _animation?.removeListener(_onAnimate);
_animation = null; _animation = null;
_controller.reset(); _controller.reset();
...@@ -1296,3 +1343,20 @@ Axis? _getPanAxis(Offset point1, Offset point2) { ...@@ -1296,3 +1343,20 @@ Axis? _getPanAxis(Offset point1, Offset point2) {
final double y = point2.dy - point1.dy; final double y = point2.dy - point1.dy;
return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical; return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical;
} }
/// This enum is used to specify the behavior of the [InteractiveViewer] when
/// the user drags the viewport.
enum PanAxis{
/// The user can only pan the viewport along the horizontal axis.
horizontal,
/// The user can only pan the viewport along the vertical axis.
vertical,
/// The user can pan the viewport along the horizontal and vertical axes
/// but not diagonally.
aligned,
/// The user can pan the viewport freely in any direction.
free,
}
...@@ -288,14 +288,52 @@ void main() { ...@@ -288,14 +288,52 @@ void main() {
expect(transformationController.value.getMaxScaleOnAxis(), minScale); expect(transformationController.value.getMaxScaleOnAxis(), minScale);
}); });
testWidgets('alignPanAxis allows panning in one direction only for diagonal gesture', (WidgetTester tester) async { testWidgets('PanAxis.free allows panning in all directions for diagonal gesture', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController(); final TransformationController transformationController = TransformationController();
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
body: Center( body: Center(
child: InteractiveViewer( child: InteractiveViewer(
alignPanAxis: true, boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value, equals(Matrix4.identity()));
// Perform a diagonal drag gesture.
final Offset childOffset = tester.getTopLeft(find.byType(SizedBox));
final Offset childInterior = Offset(
childOffset.dx + 20.0,
childOffset.dy + 20.0,
);
final TestGesture gesture = await tester.startGesture(childInterior);
await tester.pump();
await gesture.moveTo(childOffset);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Translation has only happened along the y axis (the default axis when
// a gesture is perfectly at 45 degrees to the axes).
final Vector3 translation = transformationController.value.getTranslation();
expect(translation.x, childOffset.dx - childInterior.dx);
expect(translation.y, childOffset.dy - childInterior.dy);
});
testWidgets('PanAxis.aligned allows panning in one direction only for diagonal gesture', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
panAxis: PanAxis.aligned,
boundaryMargin: const EdgeInsets.all(double.infinity), boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: transformationController, transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0), child: const SizedBox(width: 200.0, height: 200.0),
...@@ -327,14 +365,14 @@ void main() { ...@@ -327,14 +365,14 @@ void main() {
expect(translation.y, childOffset.dy - childInterior.dy); expect(translation.y, childOffset.dy - childInterior.dy);
}); });
testWidgets('alignPanAxis allows panning in one direction only for horizontal leaning gesture', (WidgetTester tester) async { testWidgets('PanAxis.aligned allows panning in one direction only for horizontal leaning gesture', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController(); final TransformationController transformationController = TransformationController();
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
body: Center( body: Center(
child: InteractiveViewer( child: InteractiveViewer(
alignPanAxis: true, panAxis: PanAxis.aligned,
boundaryMargin: const EdgeInsets.all(double.infinity), boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: transformationController, transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0), child: const SizedBox(width: 200.0, height: 200.0),
...@@ -366,6 +404,240 @@ void main() { ...@@ -366,6 +404,240 @@ void main() {
expect(translation.y, 0.0); expect(translation.y, 0.0);
}); });
testWidgets('PanAxis.horizontal allows panning in the horizontal direction only for diagonal gesture', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
panAxis: PanAxis.horizontal,
boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value, equals(Matrix4.identity()));
// Perform a diagonal drag gesture.
final Offset childOffset = tester.getTopLeft(find.byType(SizedBox));
final Offset childInterior = Offset(
childOffset.dx + 20.0,
childOffset.dy + 20.0,
);
final TestGesture gesture = await tester.startGesture(childInterior);
await tester.pump();
await gesture.moveTo(childOffset);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Translation has only happened along the x axis (the default axis when
// a gesture is perfectly at 45 degrees to the axes).
final Vector3 translation = transformationController.value.getTranslation();
expect(translation.x, childOffset.dx - childInterior.dx);
expect(translation.y, 0.0);
});
testWidgets('PanAxis.horizontal allows panning in the horizontal direction only for horizontal leaning gesture', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
panAxis: PanAxis.horizontal,
boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value, equals(Matrix4.identity()));
// Perform a horizontally leaning diagonal drag gesture.
final Offset childOffset = tester.getTopLeft(find.byType(SizedBox));
final Offset childInterior = Offset(
childOffset.dx + 20.0,
childOffset.dy + 10.0,
);
final TestGesture gesture = await tester.startGesture(childInterior);
await tester.pump();
await gesture.moveTo(childOffset);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Translation happened only along the x axis because that's the axis that
// had been set to the panningDirection parameter.
final Vector3 translation = transformationController.value.getTranslation();
expect(translation.x, childOffset.dx - childInterior.dx);
expect(translation.y, 0.0);
});
testWidgets('PanAxis.horizontal does not allow panning in vertical direction on vertical gesture', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
panAxis: PanAxis.horizontal,
boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value, equals(Matrix4.identity()));
// Perform a horizontally leaning diagonal drag gesture.
final Offset childOffset = tester.getTopLeft(find.byType(SizedBox));
final Offset childInterior = Offset(
childOffset.dx + 0.0,
childOffset.dy + 10.0,
);
final TestGesture gesture = await tester.startGesture(childInterior);
await tester.pump();
await gesture.moveTo(childOffset);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Translation didn't happen because the only axis allowed to do panning
// is the horizontal.
final Vector3 translation = transformationController.value.getTranslation();
expect(translation.x, 0.0);
expect(translation.y, 0.0);
});
testWidgets('PanAxis.vertical allows panning in the vertical direction only for diagonal gesture', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
panAxis: PanAxis.vertical,
boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value, equals(Matrix4.identity()));
// Perform a diagonal drag gesture.
final Offset childOffset = tester.getTopLeft(find.byType(SizedBox));
final Offset childInterior = Offset(
childOffset.dx + 20.0,
childOffset.dy + 20.0,
);
final TestGesture gesture = await tester.startGesture(childInterior);
await tester.pump();
await gesture.moveTo(childOffset);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Translation has only happened along the x axis (the default axis when
// a gesture is perfectly at 45 degrees to the axes).
final Vector3 translation = transformationController.value.getTranslation();
expect(translation.y, childOffset.dy - childInterior.dy);
expect(translation.x, 0.0);
});
testWidgets('PanAxis.vertical allows panning in the vertical direction only for vertical leaning gesture', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
panAxis: PanAxis.vertical,
boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value, equals(Matrix4.identity()));
// Perform a horizontally leaning diagonal drag gesture.
final Offset childOffset = tester.getTopLeft(find.byType(SizedBox));
final Offset childInterior = Offset(
childOffset.dx + 20.0,
childOffset.dy + 10.0,
);
final TestGesture gesture = await tester.startGesture(childInterior);
await tester.pump();
await gesture.moveTo(childOffset);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Translation happened only along the x axis because that's the axis that
// had been set to the panningDirection parameter.
final Vector3 translation = transformationController.value.getTranslation();
expect(translation.y, childOffset.dy - childInterior.dy);
expect(translation.x, 0.0);
});
testWidgets('PanAxis.vertical does not allow panning in horizontal direction on vertical gesture', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
panAxis: PanAxis.vertical,
boundaryMargin: const EdgeInsets.all(double.infinity),
transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value, equals(Matrix4.identity()));
// Perform a horizontally leaning diagonal drag gesture.
final Offset childOffset = tester.getTopLeft(find.byType(SizedBox));
final Offset childInterior = Offset(
childOffset.dx + 10.0,
childOffset.dy + 0.0,
);
final TestGesture gesture = await tester.startGesture(childInterior);
await tester.pump();
await gesture.moveTo(childOffset);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Translation didn't happen because the only axis allowed to do panning
// is the horizontal.
final Vector3 translation = transformationController.value.getTranslation();
expect(translation.x, 0.0);
expect(translation.y, 0.0);
});
testWidgets('inertia fling and boundary sliding', (WidgetTester tester) async { testWidgets('inertia fling and boundary sliding', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController(); final TransformationController transformationController = TransformationController();
const double boundaryMargin = 50.0; const double boundaryMargin = 50.0;
...@@ -519,7 +791,7 @@ void main() { ...@@ -519,7 +791,7 @@ void main() {
home: Scaffold( home: Scaffold(
body: Center( body: Center(
child: InteractiveViewer( child: InteractiveViewer(
alignPanAxis: true, panAxis: PanAxis.aligned,
boundaryMargin: const EdgeInsets.all(boundaryMargin), boundaryMargin: const EdgeInsets.all(boundaryMargin),
minScale: minScale, minScale: minScale,
transformationController: transformationController, transformationController: transformationController,
......
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