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)
Related
Basically I have a widget and a floatingActionButton, i want to make that widget move whenever i press the button, i managed to make it teleport to a random location, but i want it animated.
I would like my widget to animate going from his current location to a new location when pressing the button.
Also the randomNumber() function is a function that picks a random number between a minimum and a maximum.
randomNumber(min,max);
class _homePageState extends State<homePage> with SingleTickerProviderStateMixin{
late final AnimationController _animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..forward();
#override
void initState() {
super.initState();
}
double beginX = randomNumber(-1, 1);
double beginY = randomNumber(-1, 1);
double endX = randomNumber(-1, 1);
double endY = randomNumber(-1, 1);
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
late final Animation<Offset> _animation = Tween<Offset>(
begin: Offset(beginX, beginY),
end: Offset(endX, endY),
).animate(_animationController);
return Scaffold(
body: Center(
child: SlideTransition(
child: Container(
height: 100,
width: 100,
color: Colors.red,
),
position: _animation,
)
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
beginX = endX; // here i wanted the widget's old end position to become it's new begin position
beginY = endY;
endX = randomNumber(-1, 1);
endY = randomNumber(-1, 1);
_animationController..forward();
});
},
),
);
}
}
you could set up your own animations and do it that way, nut it would be a lot simpeler to just use a Stack and an AnimatedPositioned then you'd just have to update some random position values and rebuild the widget.
the docs have all the info
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!
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 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?
My app is a simple Column with 3 Text widgets wrapped in SlideTransitions. My goal is for the app to load with nothing on the screen, and then animate these Text widgets from the bottom (off screen) up into the screen (settling in the middle).
return Column(
children: <Widget>[
SlideTransition(
position:_curve1,
child: Text('Hello 1')
),
SlideTransition(
position:_curve1,
child: Text('Hello 2')
),
SlideTransition(
position:_curve1,
child: Text('Hello 3')
),
]
);
The problem is that when defining the animation, you have to specify their initial Offset.
#override
void initState(){
...
Offset beginningOffset = Offset(0.0,20.0);
_curve1 = Tween<Offset>(begin: beginningOffset, end: Offset.zero).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut)));
...
}
Here you can see that beginningOffset is specified as Offset(0.0, 20.0) which does successfully position the widgets off screen. But what happens if I suddenly run this on a tablet? Since Offset is defined as units of the height of the widget, then this is obviously not a good way to position a widget off screen.
Since the animation is defined in "initState()" ... I see no way to use something like MediaQuery.of(context) ... since we don't have context there.
In native iOS this would be simple. Within "viewDidLoad" you would get the frame from self.view. You'd position each text field at the edge of the frame explicitly. You'd then animate a constraint change, positioning them where you want them. Why must this be so hard in Flutter?
I find it especially curious that this exact scenario (starting an animation just off screen) is totally not covered in any of the examples I could find. For instance:
https://github.com/flutter/website/blob/master/examples/_animation/basic_staggered_animation/main.dart
Seems like almost every type of animation is featured here EXCEPT an explicit off screen animation on load... Perhaps I'm just missing something.
EDIT: ThePeanut has solved it! Check his "Second Update".
You might want to try using the addPostFrameCallback method in your initState.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){
// Schedule code execution once after the frame has rendered
print(MediaQuery.of(context).size.toString());
});
}
Flutter Api Docs Link
OR you can also use a Future for this:
#override
void initState() {
super.initState();
new Future.delayed(Duration.zero, () {
// Schedule a zero-delay future to be executed
print(MediaQuery.of(context).size.toString());
});
}
Hope this helps.
UPDATED
A bit of a unusual way to do it, but it really does the thing you need.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
Animation<Offset> animation;
AnimationController animationController;
#override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
animation = Tween<Offset>(
begin: Offset(0.0, 1.0),
end: Offset(0.0, 0.0),
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.fastLinearToSlowEaseIn,
));
Future<void>.delayed(Duration(seconds: 1), () {
animationController.forward();
});
}
#override
void dispose() {
// Don't forget to dispose the animation controller on class destruction
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Stack(
alignment: Alignment.center,
fit: StackFit.expand,
children: <Widget>[
SlideTransition(
position: animation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircleAvatar(
backgroundImage: NetworkImage(
'https://pbs.twimg.com/media/DpeOMc3XgAIMyx_.jpg',
),
radius: 50.0,
),
],
),
),
],
),
);
}
}
It works this way:
1. We create a Stack of items with options to expand any child inside it.
2. Wrap each slide you want to display in to a Column with center alignment of children. The Column will take 100% of the size of the Stack.
3. Set the beginning Offset for animation to Offset(0.0, 1.0).
Keep in mind that dx and dy in Offset are not pixels or something like that, but the ratio of Widget's width or height. For example: if your widget's width is 100.0 and you put 0.25 as dx - it will result in moving your child to the right by 25.0 points.
So setting offset to (0.0, 1.0) will move the Column offscreen to the bottom by it's 100% height (this is how many page transitions work in Flutter).
4. Animate the Column back to it's original position after a 1 second delay.
SECOND UPDATE
This code calculates the offset based on the screen size and widget size.
PS. There might be a better way of doing this that I don't know of.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Page(),
);
}
}
class Page extends StatefulWidget {
#override
State<StatefulWidget> createState() => _PageState();
}
class _PageState extends State<Page> with SingleTickerProviderStateMixin {
// Set the initial position to something that will be offscreen for sure
Tween<Offset> tween = Tween<Offset>(
begin: Offset(0.0, 10000.0),
end: Offset(0.0, 0.0),
);
Animation<Offset> animation;
AnimationController animationController;
GlobalKey _widgetKey = GlobalKey();
#override
void initState() {
super.initState();
// initialize animation controller and the animation itself
animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
animation = tween.animate(animationController);
Future<void>.delayed(Duration(seconds: 1), () {
// Get the screen size
final Size screenSize = MediaQuery.of(context).size;
// Get render box of the widget
final RenderBox widgetRenderBox =
_widgetKey.currentContext.findRenderObject();
// Get widget's size
final Size widgetSize = widgetRenderBox.size;
// Calculate the dy offset.
// We divide the screen height by 2 because the initial position of the widget is centered.
// Ceil the value, so we get a position that is a bit lower the bottom edge of the screen.
final double offset = (screenSize.height / 2 / widgetSize.height).ceilToDouble();
// Re-set the tween and animation
tween = Tween<Offset>(
begin: Offset(0.0, offset),
end: Offset(0.0, 0.0),
);
animation = tween.animate(animationController);
// Call set state to re-render the widget with the new position.
this.setState((){
// Animate it.
animationController.forward();
});
});
}
#override
void dispose() {
// Don't forget to dispose the animation controller on class destruction
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
fit: StackFit.loose,
children: <Widget>[
SlideTransition(
position: animation,
child: CircleAvatar(
key: _widgetKey,
backgroundImage: NetworkImage(
'https://pbs.twimg.com/media/DpeOMc3XgAIMyx_.jpg',
),
radius: 50.0,
),
),
],
);
}
}
I found this solution, to have the exact value of off-screen.
Screen size / 2 / widget size + half of 1 offset
Without the half offset, only half of the widget is off-screen.
final widgetHeight = 190;
final offsetScreen = MediaQueryData.fromWindow(WidgetsBinding.instance!.window).size.height / 2 / widgetHeight + 0.5;