I am trying to seamlessly move, rotate and scale a widget that is positioned within a stack. Since Pan and Scale Gesture Recognizers cannot be used together within the GestureDetector widget, I am trying to use scale to derive offset.
However, the object scales in a weird way when attempting to pan and scale at the same time, and the object scales and moves away of my pinching fingers. If done separately, they behave normally.
Based on a couple of posts, I've attempted the following:
import 'package:app/features/_shared_widgets/vertical_spacing.dart';
import 'package:app/model/piece.dart';
import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';
import 'dart:ui' as ui;
class MoveableStackItem extends StatefulWidget {
final Piece piece;
MoveableStackItem({#required this.piece});
#override
State<StatefulWidget> createState() {
return _MoveableStackItemState();
}
}
class _MoveableStackItemState extends State<MoveableStackItem> {
double _xPosition;
double _yPosition;
double _rotation;
double _scale;
Offset _startingFocalPoint;
Offset _previousOffset;
double _previousScale;
#override
void initState() {
super.initState();
_xPosition = widget.piece.position.x;
_yPosition = widget.piece.position.y;
_rotation = widget.piece.position.angle;
_scale = widget.piece.position.scale;
}
#override
Widget build(BuildContext context) {
return Positioned(
top: _yPosition,
left: _xPosition,
child: GestureDetector(
onScaleStart: (ScaleStartDetails details) {
setState(() {
_startingFocalPoint = details.focalPoint;
_previousOffset = Offset(_xPosition, _yPosition);
_previousScale = _scale;
});
},
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
_scale = _previousScale * details.scale;
// Ensure that item under the focal point stays in the same place despite zooming
final Offset normalizedOffset =
(_startingFocalPoint - _previousOffset) / _previousScale;
_xPosition = (details.focalPoint - normalizedOffset * _scale).dx;
_yPosition = (details.focalPoint - normalizedOffset * _scale).dy;
if (details.rotation != 0) _rotation = details.rotation;
});
},
child: Transform.rotate(
angle: _rotation,
child: Transform.scale(
scale: _scale,
child: Container(
width: 200,
child: FadeInImage.memoryNetwork(
fit: BoxFit.contain,
image: widget.piece.imageUrl,
placeholder: kTransparentImage,
placeholderErrorBuilder: (context, url, error) =>
Icon(Icons.error),
),
),
),
),
),
);
}
}
I also tried Transform.translate with a position of top: 0, and left: 0 with the same results. Can anyone detect anything that might be upsetting the translation?
Related
I'm writing a flutter program in which you can move a widget with your finger by using GestureDetector instead of Draggable Widget.
Here is my code. I use Transform.translate to move.
import 'package:flutter/material.dart';
class AAA extends StatefulWidget {
#override
AAAState createState() => AAAState();
}
class AAAState extends State<AAA> {
double x = 0.0;
double y = 0.0;
#override
Widget build(BuildContext context) {
return Transform.translate(
offset: Offset(x, y),
child: GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
RenderBox getBox = context.findRenderObject();
final localOffset = getBox.globalToLocal(details.globalPosition);
setState(() {
x = localOffset.dx;
y = localOffset.dy;
});
},
child: ElevatedButton(
onPressed: null,
child: Text('Button'),
),
),
);
}
}
//Actually I'm using AAA widget in a List like this.
Container(
width: double.infinity,
height: double.infinity,
child: Column(
children: widgetList,
),
),
But in my code, when I move the widget with my finger, the origin of the widget is constantly fixed at top-left.
I tried using Transform instead of Transform.translate and changing origin: property, but it wasn't going well.
Like this:
gif image
I want the origin to be at the position where I touched first.
Sorry for my poor English, but give me some advice!
With the Interactive viewer documentation i came to know that we can autoscroll to a particular position in Interactive viewer with toScene method.But i tried but everything in vain.How to autoscroll to an offset given the positions in interactive viewer
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class MyPlanet extends StatefulWidget {
#override
_MyPlanetState createState() => _MyPlanetState();
}
class _MyPlanetState extends State<MyPlanet> {
final TransformationController transformationController =
TransformationController();
#override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) async {
Timer(Duration(seconds: 5), () {
setState(() {
transformationController.value = Matrix4.identity()
..translate(800, 0.0);
transformationController.toScene(Offset(800, 0.0));
});
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return InteractiveViewer(
minScale: 0.5,
maxScale: 1,
constrained: false,
transformationController: transformationController,
child: Container(
height: 896,
width: 2000,
child: Image.asset(
'assets/images/rain_forest.png',
fit: BoxFit.cover,
height: double.infinity,
),
),
);
}
}
Thanks #pskink.
Autoscroll in interactive viewer can be achieved by using TransformationController and matrix4
#override
void initState() {
TransformationController transformationController =
TransformationController();
transformationController.value = Matrix4.identity()
..translate(-200.0, 0.0); // translate(x,y);
}
Above solution not work for me - it move whole image on the screen, not only set needed position. I resolve issue with this example https://api.flutter.dev/flutter/widgets/InteractiveViewer/transformationController.html
Define end point for scroll for example by link:
Matrix4 scrollEnd = Matrix4.identity();
scrollEnd.setEntry(1, 3, -700); // y = 700
_controllerReset.reset();
_animationReset = Matrix4Tween(
begin: Matrix4.identity(),
end: scrollEnd,
).animate(_controllerReset);
_animationReset!.addListener(_onAnimateReset);
_controllerReset.forward();
I'm new to Flutter and Dart. I used the example project found on the Flutter Docs to build my app.
This is the code:
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
main() {
runApp(MaterialApp(home: PhysicsCardDragDemo()));
}
class PhysicsCardDragDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: DraggableCard(
child: FlutterLogo(
size: 128,
),
),
);
}
}
/// A draggable card that moves back to [Alignment.center] when it's
/// released.
class DraggableCard extends StatefulWidget {
final Widget child;
DraggableCard({this.child});
#override
_DraggableCardState createState() => _DraggableCardState();
}
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
AnimationController _controller;
/// The alignment of the card as it is dragged or being animated.
///
/// While the card is being dragged, this value is set to the values computed
/// in the GestureDetector onPanUpdate callback. If the animation is running,
/// this value is set to the value of the [_animation].
Alignment _dragAlignment = Alignment.center;
Animation<Alignment> _animation;
/// Calculates and runs a [SpringSimulation].
void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
// Calculate the velocity relative to the unit interval, [0,1],
// used by the animation controller.
final unitsPerSecondX = pixelsPerSecond.dx / size.width;
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;
const spring = SpringDescription(
mass: 30,
stiffness: 1,
damping: 1,
);
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
_controller.animateWith(simulation);
}
#override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {
_controller.stop();
},
onPanUpdate: (details) {
setState(() {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
});
},
onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}
}
I'm trying to animate other properties of the DraggableCard() besides its position; specifically I want to dynamically change its rotation and opacity based on the horizontal drag.
In order to do that, I'm trying to get the value of _dragAlignment so I can feed it to a Transform.rotate(), but the value corresponds to a coordinate and I only need to read the value of the x coordinate.
So in other words I need to extract the X value from the class Alignment(x, y) which corresponds to the _dragAlignment. I have tried many things but nothing worked.
I searched here and other places for solutions but couldn't find any help, maybe I'm not asking the right questions.
I'm sorry if this is a stupid question and if it's been asked before.
I think you can do something like this,
var align = Alignment(1,2);
var xCoord = align.x;
var yCoord = align.y;
Hope that works!
Thank you Shri Hari for your help :)
I solved my problem by declaring
var xCoord = 0.0;
Where _dragAlignment is also declared and than using
child: Transform.rotate(
angle: -_dragAlignment.x/200
Directly in the GestureDetector().
I also had to edit the values in onPanUpdate because it looked like it wasn't dragging anymore but you just need to increase the details.delta.dx...
Like This:
return GestureDetector(
onPanDown: (details) {
_controller.stop();
},
onPanUpdate: (details) {
//print(_dragAlignment);
setState(() {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 300), // X drag sensitivity
details.delta.dy / (size.height / 9), //Y drag sensitivity
);
});
},
onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
},
child: Align(
alignment: _dragAlignment,
child: Transform.rotate(
angle: -_dragAlignment.x/200,
child: Card(
child: widget.child,
),
)
),
);
I have a container upon which I'm detecting gestures so that I can scroll through an image that I'm creating with custom painter, like so
class CustomScroller extends StatefulWidget {
#override
_CustomScrollerState createState() => _CustomScrollerState();
}
class _CustomScrollerState extends State<CustomScroller>
with SingleTickerProviderStateMixin {
AnimationController _controller;
double dx = 0;
Offset velocity = Offset(0, 0);
double currentLocation = 0;
#override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 1));
super.initState();
}
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
double width = MediaQuery.of(context).size.width;
return GestureDetector(
onHorizontalDragUpdate: (DragUpdateDetails dragUpdate) {
dx = dragUpdate.delta.dx;
currentLocation =
currentLocation + (dx * 10000) / (width * _absoluteRatio);
setState(() {});
},
onHorizontalDragEnd: (DragEndDetails dragUpdate) {
velocity = dragUpdate.velocity.pixelsPerSecond;
//_runAnimation(velocity, size);
},
child: Container(
width: width,
height: 50,
child: Stack(
children: <Widget>[
CustomPaint(
painter: NewScrollerPainter(1, true, currentLocation),
size: Size.fromWidth(width)),
],
),
),
);
}
}
I've written some code so that on a drag the pointer's dx value is used to calculate currentLocation, which is used by the customPainter 'NewScrollerPaint' to determine what area it needs to paint.
I essentially want a fling animation, whereby releasing from a drag with a velocity, the custom painter 'NewScrollerPainter' will continue to scroll until it is out of momentum. I believe this requires the value of currentLocation to be calculated from the velocity, and then returned for NewScrollerPainter to be updated, but I've spent a few hours looking through Flutter's animation docs and playing around with open source code but I'm still not sure how I would go about doing this. Could anyone shed some light onto this issue?
class CustomScroller extends StatefulWidget {
#override
_CustomScrollerState createState() => _CustomScrollerState();
}
class _CustomScrollerState extends State<CustomScroller>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 1));
super.initState();
}
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return GestureDetector(
onHorizontalDragUpdate: (DragUpdateDetails dragUpdate) {
_controller.value += dragUpdate.primaryDelta / width;
},
onHorizontalDragEnd: (DragEndDetails dragEnd) {
final double flingVelocity = dragEnd.primaryVelocity / width;
const spring = SpringDescription(
mass: 30,
stiffness: 1,
damping: 1,
);
final simulation = SpringSimulation(spring, _controller.value, 0, flingVelocity); //if you want to use a spring simulation returning to the origin
final friction = FrictionSimulation(1.5, _controller.value, -(flingVelocity.abs())); //if you want to use a friction simulation returning to the origin
//_controller.animateWith(friction);
//_controller.animateWith(simulation);
_controller.fling(velocity: -(flingVelocity.abs())); //a regular fling based on the velocity you were dragging your widget
},
child: Stack(
children: [
SlideTransition(
position: Tween<Offset>(begin: Offset.zero, end: const Offset(1, 0))
.animate(_controller),
child: Align(
alignment: Alignment.centerLeft,
child: CustomPaint(
painter: NewScrollerPainter(),
size: Size.fromWidth(width),
child: SizedBox(width: width, height: 50)
),
)
)
]
)
);
}
}
I see you are trying to move in the X axis only (you're usign onHorizontalDragUpdate and End) so you can use the values primaryDelta and primaryVelocity, that already gives you the calculations of the position and velocity in the primary axis (horizontal in this case).
In onHorizontalDragUpdate you mve the controller value adding itself the position of the dragging (positive you're moving away of the place you first tapped, negative you're moving back, 0 you haven't moved or dragged).
In onHorizontalDragEnd you calculate the velocity with primaryVelocity and the same logic (positive you're moving away, negative you're returning, 0 there is no velocity, bascially you stopped moving before your drag ended). You can use simulations or just the fling method and pass the velocity you have (positive you want to end the animation, negative you velocity you want to dismiss it and 0 you don't want to move).
At the end the only thing you need to do is wrap your widget you cant to move with the controller in a SlideTransition with a Tween animated by the controller beggining at zero and end where you want to end it (in my case I want to move it from left to right)
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.
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.
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))
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.
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;
});
}
}