Flutter: How to implement Rotate and Pan/Move gesture for any container?
Solution 1
In scale
-related events, you can use the focalPoint
to calculate panning, in addition to scaling (zooming). Panning while zooming can also be supported.
Demo:
Here's the code used for the above demo:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ZoomAndPanDemo(),
);
}
}
class ZoomAndPanDemo extends StatefulWidget {
@override
_ZoomAndPanDemoState createState() => _ZoomAndPanDemoState();
}
class _ZoomAndPanDemoState extends State<ZoomAndPanDemo> {
Offset _offset = Offset.zero;
Offset _initialFocalPoint = Offset.zero;
Offset _sessionOffset = Offset.zero;
double _scale = 1.0;
double _initialScale = 1.0;
@override
Widget build(BuildContext context) {
return GestureDetector(
onScaleStart: (details) {
_initialFocalPoint = details.focalPoint;
_initialScale = _scale;
},
onScaleUpdate: (details) {
setState(() {
_sessionOffset = details.focalPoint - _initialFocalPoint;
_scale = _initialScale * details.scale;
});
},
onScaleEnd: (details) {
setState(() {
_offset += _sessionOffset;
_sessionOffset = Offset.zero;
});
},
child: Transform.translate(
offset: _offset + _sessionOffset,
child: Transform.scale(
scale: _scale,
child: FlutterLogo(),
),
),
);
}
}
Side note: even though events like onHorizontalDragUpdate
do not cause runtime exception when used with scale-related events, they still cause conflict and will result in an inferior UX.
It's also worth-noting that InteractiveViewer
is a built-in Flutter widget that can handle most of your needs, so you might not need to use GestureDetector
and Transform
at all.
Solution 2
We can use the focalPoint field of ScaleUpdateDetails object, which we get as an argument in the onScaleUpdate function.
Solution related to the above example: We need to update the onScaleUpdate method.
onScaleUpdate: (scaleUpdates) {
lastRotation += scaleUpdates.rotation;
var offset = scaleUpdates.focalPoint;
xOffset = offset.dx;
yOffset = offset.dy;
setState(() => _scale = _previousScale * scaleUpdates.scale);
}
Change 'rect' field of Positioned Widget in above code.
rect: Rect.fromPoints(Offset(xOffset - 125.0, yOffset - 50.0),
Offset(xOffset + 250.0, yOffset + 100.0))
Solution 3
Default GestureRecognizer
does not support the recognition of pan/drag and scaling at the same time. I think this is the bug and it should be fixed. To implement such behavior - you need to build you own recogizer RawGestureDetector
based on ImmediateMultiDragGestureRecognizer
gestures.
I have already implemented class PanAndScalingGestureRecognizer
here: https://gist.github.com/comm1x/8ffffd08417053043e079878b4bd8d03
So you can see full example or just copypaste and use.
Solution 4
I using Pointer Listener to implement my custom gesture detector.
For anyone facing the same problem, just take a look on package gesture_x_detector
Supports (tap, double-tap, scale(start, update, end), move(start, update, end) and long-press. All types can be used simultaneously.
Example:
import 'package:flutter/material.dart';
import 'package:gesture_x_detector/gesture_x_detector.dart';
void main() {
runApp(
MaterialApp(
home: XGestureExample(),
),
);
}
class XGestureExample extends StatefulWidget {
@override
_XGestureExampleState createState() => _XGestureExampleState();
}
class _XGestureExampleState extends State<XGestureExample> {
String lastEventName = 'Tap on screen';
@override
Widget build(BuildContext context) {
return XGestureDetector(
child: Material(
child: Center(
child: Text(
lastEventName,
style: TextStyle(fontSize: 30),
),
),
),
doubleTapTimeConsider: 300,
longPressTimeConsider: 350,
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
onMoveStart: onMoveStart,
onMoveEnd: onMoveEnd,
onMoveUpdate: onMoveUpdate,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
bypassTapEventOnDoubleTap: false,
);
}
void onScaleEnd() {
setLastEventName('onScaleEnd');
print('onScaleEnd');
}
void onScaleUpdate(changedFocusPoint, scale) {
setLastEventName('onScaleUpdate');
print(
'onScaleUpdate - changedFocusPoint: $changedFocusPoint ; scale: $scale');
}
void onScaleStart(initialFocusPoint) {
setLastEventName('onScaleStart');
print('onScaleStart - initialFocusPoint: ' + initialFocusPoint.toString());
}
void onMoveUpdate(localPos, position, localDelta, delta) {
setLastEventName('onMoveUpdate');
print('onMoveUpdate - pos: ' + localPos.toString());
}
void onMoveEnd(pointer, localPos, position) {
setLastEventName('onMoveEnd');
print('onMoveEnd - pos: ' + localPos.toString());
}
void onMoveStart(pointer, localPos, position) {
setLastEventName('onMoveStart');
print('onMoveStart - pos: ' + localPos.toString());
}
void onLongPress(pointer, localPos, position) {
setLastEventName('onLongPress');
print('onLongPress - pos: ' + localPos.toString());
}
void onDoubleTap(localPos, position) {
setLastEventName('onDoubleTap');
print('onDoubleTap - pos: ' + localPos.toString());
}
void onTap(pointer, localPos, position) {
setLastEventName('onTap');
print('onTap - pos: ' + localPos.toString());
}
void setLastEventName(String eventName) {
setState(() {
lastEventName = eventName;
});
}
}
Ankur Prakash
Strength: Data structures and Algorithms Performance optimization of the app Client interaction and understanding requirememnts Using best design patterns and Technology for the app (DB, UI, Cost effective)
Updated on December 09, 2022Comments
-
Ankur Prakash over 1 year
I have implemented the Scale gesture for the container. Also, I have added onHorizontalDragUpdate and onVerticalDragUpdate. But when I try to add both, I get an exception saying can't implement both with Scale gesture. Even for Pan gesture, it throws the same exception. Below is my code:
import 'package:flutter/material.dart'; import 'package:vector_math/vector_math_64.dart' hide Colors; import 'dart: math' as math; class HomeScreen extends StatefulWidget { @override State<StatefulWidget> createState() { return HomeState(); } } class HomeState extends State<HomeScreen> { double _scale = 1.0; double _previousScale; var yOffset = 400.0; var xOffset = 50.0; var rotation = 0.0; var lastRotation = 0.0; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.amber, body: Stack( children: <Widget>[ stackContainer(), ], ), ); } Widget stackContainer() { return Stack( children: <Widget>[ Positioned.fromRect( rect: Rect.fromPoints( Offset(xOffset, yOffset), Offset(xOffset+250.0, yOffset+100.0)), child: GestureDetector( onScaleStart: (scaleDetails) { _previousScale = _scale; print(' scaleStarts = ${scaleDetails.focalPoint}'); }, onScaleUpdate: (scaleUpdates){ //ScaleUpdateDetails rotation += lastRotation - scaleUpdates.rotation; lastRotation = scaleUpdates.rotation; print("lastRotation = $lastRotation"); print(' scaleUpdates = ${scaleUpdates.scale} rotation = ${scaleUpdates.rotation}'); setState(() => _scale = _previousScale * scaleUpdates.scale); }, onScaleEnd: (scaleEndDetails) { _previousScale = null; print(' scaleEnds = ${scaleEndDetails.velocity}'); }, child: Transform( transform: Matrix4.diagonal3( Vector3(_scale, _scale, _scale))..rotateZ(rotation * math.pi/180.0), alignment: FractionalOffset.center, child: Container( color: Colors.red, ), ) , ), ), ], ); } }
I wanted to move around the red subview and rotate along with the scale.