I'm trying to rotate a widget (which is not part of the problem as it handles the rotation itself via constructor parameter) based on an animation which interpolates between the previous rotation position a new one obtained with a plugin function. The value of that function (FlutterCompass.events.listen) updates asynchroniously on a regular basis, and it rebuilds the Tween objetc everytime in order to represent the update of the position of the widget. Here's my code:
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'package:flutter_compass/flutter_compass.dart';
import 'package:compass_test/compass.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin{
double _direction;
double _angle = 0.0;
Animation<double> _animation;
AnimationController _animationController;
Tween<double> _tween;
#override
void initState() {
super.initState();
_animationController =
AnimationController(duration: Duration(milliseconds: 400), vsync: this);
_tween = Tween<double>(begin: 0.0, end: 0.0);
_animation = _tween.animate(_animationController)
..addListener(() {
setState(() {
_angle =_animationController.value;
});
});
_direction = 0;
FlutterCompass.events.listen((double direction) {
print(_animationController.status);
if(_direction !=direction){
_tween = Tween<double>(
begin: _direction,
end: direction);
_animationController.reset();
_tween.animate(_animationController);
_animationController.forward();
}
_direction = direction;
});
}
#override
void dispose(){
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
Center(
child: Container(
margin: EdgeInsets.only(top: 100),
child: Compass(height: 300, width: 300, angleToNorth: _angle)
)
)
],
)
),
);
}
}
However, as I've been able to see with some debug, the values returned from _animationController.value only vary from 0.0 to 1.0, which is not what I tought was supposed to happen: I expected them to vary from the previous value of _direction to the new direction value. How can I achive that?
Thanks in advance
That is how animation controllers are suppose to work. With some extra lines you can even add a curve to the change in values from 0.0 to 1.0. But the values will always go from 0.0 to 1.0. So what you can do is update _angles value like this _angle = degree of angle * _animationController.value. So lets say your angle is 50 degree. So when you start your animation the animation controller's value will start from 0.0 which will be multiplied by 50 giving you 0. And as the animation controllers value moves from 0.0 to 1.0 the _angle's value will also change and in the end giving you 50 as 1.0 * 50 is 50! But as you mentioned you just want to rotate your widget. So instead of this you can use a tween animation builder of type double and update the end value if you want to run the animation again; the animation will continue from the previous value.
Related
I want bubble to move smoothly between edges of the screen tracking random curves.
This is the code I have
class BubbleAnimator extends StatefulWidget {
const BubbleAnimator({Key key}) : super(key: key);
#override
_BubbleAnimatorState createState() => _BubbleAnimatorState();
}
class _BubbleAnimatorState extends State<BubbleAnimator> {
double x;
double y;
#override
void initState() {
super.initState();
x = math.Random().nextDouble()*2-1;
y = math.Random().nextDouble()*2-1;
}
#override
Widget build(BuildContext context) {
return Bubble(
x: x,
y: y,
);
}
}
class Bubble extends StatelessWidget {
final double x;
final double y;
const Bubble({Key key, this.x, this.y}) : super(key: key);
#override
Widget build(BuildContext context) {
return AnimatedAlign(
duration: Duration(milliseconds: 300),
alignment: Alignment(x, y),
child: GestureDetector(
onTap: () {
print("tapped");
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
decoration:
BoxDecoration(shape: BoxShape.circle, color: Colors.blue),
height: 30,
width: 30,
)),
);
}
}
How could I make my BubbleAnimator to move by curve?
For example, let my bubble shows up in the middle of the screen. Bubble then moves toward random edge of screen by surfing some smooth curve. Hitting the edge bubble then changes it's direction to some other edge tracking another unpredictable curve.
AnimatedAlign like any other Animated Widget (AnimatedContainer, etc) needs to be inside a StatefulWidget.
Your x and y values can't be final as they need to change to animate your Alignment.
Either move your BubbleWidget logic inside your BubbleAnimator or move everything to Bubble and convert that one into a StatefulWidget.
You'd need to call setState every time you change your x and y variables.
Im looking forward to test flutter memory/cpu usage. But I'm totaly lost at what widget do I pick for:
widget will contain custom canvas drawing(full screen)
widget must update itself 30 times per second(calling repaint from 0 each time)
in general, we have our own engine that revolves around uiview/surfaceview. I want to write same stuff on dart, connect to server, get same data, get same picture. But I dont unrestand what widget to take. As far as I see, I'l pick statefull widget and will change it state 30 timer per sec with timer. But that's not sounds right to me thought
You can use Ticker, which is the same mechanism that animations (i.e. AnimationController) use to update every frame.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(const CanvasWidget());
class CanvasWidget extends StatefulWidget {
const CanvasWidget({super.key});
#override
State<CanvasWidget> createState() => _CanvasWidgetState();
}
class _CanvasWidgetState extends State<CanvasWidget> with SingleTickerProviderStateMixin {
final drawState = DrawState();
Ticker? ticker;
#override
void initState() {
super.initState();
ticker = createTicker(tick);
ticker!.start();
}
#override
void dispose() {
ticker?.stop();
ticker?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: CustomPaint(
painter: MyPainter(drawState),
),
);
}
void tick(Duration elapsed) {
var t = elapsed.inMicroseconds * 1e-6;
double radius = 100;
drawState.x = radius * math.sin(t);
drawState.y = radius * math.cos(t);
setState(() {});
}
}
class DrawState {
double x = 0, y = 0;
}
class MyPainter extends CustomPainter {
final DrawState state;
MyPainter(this.state);
#override
void paint(Canvas canvas, Size size) {
var paint = Paint()..color = Colors.red;
canvas.drawCircle(Offset(state.x + size.width * 0.5, state.y + size.height * 0.5), 20, paint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
So I'm trying to create an animation in Flutter that requires a different outcome every time the user presses a button.
I've implemented the following code according to the Flutter Animations tutorial and created a function to update it.
class _RoulettePageWidgetState extends State<RoulettePageWidget>
with SingleTickerProviderStateMixin {
Animation<double> _animation;
Tween<double> _tween;
AnimationController _animationController;
int position = 0;
#override
void initState() {
super.initState();
_animationController =
AnimationController(duration: Duration(seconds: 2), vsync: this);
_tween = Tween(begin: 0.0, end: 100.0);
_animation = _tween.animate(_animationController)
..addListener(() {
setState(() {});
});
}
void setNewPosition(int newPosition) {
_tween = Tween(
begin: 0.0,
end: math.pi*2/25*newPosition);
_animationController.reset();
_tween.animate(_animationController);
_animationController.forward();
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Center(
child: Transform.rotate(
angle: _animationController.value,
child: Icon(
Icons.arrow_upward,
size: 250.0,
),
)),
Expanded(
child: Container(),
),
RaisedButton(
child: Text('SPIN'),
onPressed: () {
setState(() {
setNewPosition(math.Random().nextInt(25));
});
},
)
],
)
);
}
}
As you can see I'm updating the _tween's begin: and end: but this doesn't seem to change the animation.
So what should I be doing to create a 'different' animation every time the users presses the button?
The general idea is to make the animations build upon each other with a random new value so for example:
first spin: 0 -> 10
second spin: 10 -> 13
third spin: 13 -> 18
... etc
So I wondered if I could update the animation, or should I create a new animation every time?
Another thing I could think of was tracking the positions and use the same animation every time (0.0 -> 100.0) to act as a percentage of the transfer.
So instead of creating a new animation from 10 -> 15 I would be doing something like:
currentValue = 10 + (15-10)/100*_animationController.value
I'm going to skip your code a bit, and focus on what you're really asking:
The general idea is to make the animations build upon each other with a random new value so for example:
first spin: 0 -> 10
second spin: 10 -> 13
third spin: 13 -> 18
... etc
With an explicit animation like this, there are three objects you are interested in:
a controller, which is a special kind of Animation that simply generates values linearly from its lower to its upper bound (both doubles, typically 0.0 and 1.0). You can control the flow of the animation - send it running forward, reverse it, stop it, or reset it.
a tween, which isn't an Animation but rather an Animatable. A tween defines the interpolation between two values, which don't even have to be numbers. It implements a transform method under the hood that takes in the current value of an animation and spits out the actual value you want to work with: another number, a color, a linear gradient, even a whole widget. This is what you should use to generate your angles of rotation.
an animation, which is the animation whose value you're actually going to work with (so this is where you'd grab values to build with). You get this by giving your tween a parent Animation to transform - this might be your controller directly but can also be some other sort of animation you've built on it (like a CurvedAnimation, which would give you easing or bouncy/elastic curves and so on). Flutter's animations are highly composable that way.
Your code is failing largely because you're not actually using the top-level animation you created in your build method and you're creating a new tween and animation every time you call setNewPosition. You can use the same tween and animation for multiple animation "cycles" - simply change the begin and end properties of the existing tween and it bubbles up to the animation. That ends up something like this:
class _RoulettePageWidgetState extends State<RoulettePageWidget>
with SingleTickerProviderStateMixin {
Animation<double> _animation;
Tween<double> _tween;
AnimationController _animationController;
math.Random _random = math.Random();
int position = 0;
double getRandomAngle() {
return math.pi * 2 / 25 * _random.nextInt(25);
}
#override
void initState() {
super.initState();
_animationController =
AnimationController(duration: Duration(seconds: 2), vsync: this);
_tween = Tween(begin: 0.0, end: getRandomAngle());
_animation = _tween.animate(_animationController)
..addListener(() {
setState(() {});
});
}
void setNewPosition() {
_tween.begin = _tween.end;
_animationController.reset();
_tween.end = getRandomAngle();
_animationController.forward();
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Center(
child: Transform.rotate(
angle: _animation.value,
child: Icon(
Icons.arrow_upward,
size: 250.0,
),
)),
Expanded(
child: Container(),
),
RaisedButton(
child: Text('SPIN'),
onPressed: setNewPosition,
)
],
)
);
}
}
Hope that helps!
While working, in no situation will you actually want to make these animations within your layout as explained by #filleduchaos.
This is under optimized, as you're rebuilding far more than you should for the animation. And it's a pain to write yourself.
You'll want to use the AnimatedWidget family for this. They are divided into two
kinds:
XXTransition
AnimatedXX
The first is a low layer that consumes an Animation and listens to it so that you don't need to do that ugly :
..addListener(() {
setState(() {});
});
The second handles the remaining pieces: AnimationController, TickerProvider and Tween.
This makes using animations much easier as it's almost entirely automatical.
In your case a rotation example would be as followed:
class RotationExample extends StatefulWidget {
final Widget child;
const RotationExample({
Key key,
this.child,
}) : super(key: key);
#override
RotationExampleState createState() {
return new RotationExampleState();
}
}
class RotationExampleState extends State<RotationExample> {
final _random = math.Random();
double rad = 0.0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _rotate,
child: AnimatedTransform(
duration: const Duration(seconds: 1),
alignment: Alignment.center,
transform: Matrix4.rotationZ(rad),
child: Container(
color: Colors.red,
height: 42.0,
width: 42.0,
),
),
);
}
void _rotate() {
setState(() {
rad = math.pi * 2 / 25 * _random.nextInt(25);
});
}
}
Easier right?
The irony is that Flutter forgot to provide an AnimatedTransform (even although we have many others !). But no worries, I made it for you!
The AnimatedTransform implementation is as followed :
class AnimatedTransform extends ImplicitlyAnimatedWidget {
final Matrix4 transform;
final AlignmentGeometry alignment;
final bool transformHitTests;
final Offset origin;
final Widget child;
const AnimatedTransform({
Key key,
#required this.transform,
#required Duration duration,
this.alignment,
this.transformHitTests = true,
this.origin,
this.child,
Curve curve = Curves.linear,
}) : assert(transform != null),
assert(duration != null),
super(
key: key,
duration: duration,
curve: curve,
);
#override
_AnimatedTransformState createState() => _AnimatedTransformState();
}
class _AnimatedTransformState
extends AnimatedWidgetBaseState<AnimatedTransform> {
Matrix4Tween _transform;
#override
void forEachTween(TweenVisitor<dynamic> visitor) {
_transform = visitor(_transform, widget.transform,
(dynamic value) => Matrix4Tween(begin: value));
}
#override
Widget build(BuildContext context) {
return Transform(
alignment: widget.alignment,
transform: _transform.evaluate(animation),
transformHitTests: widget.transformHitTests,
origin: widget.origin,
child: widget.child,
);
}
}
I will submit a pull request so that in the future you won't need this bit of code.
If you want to reverse your animation with a different path (go/back way). Try this.
In your setNewPosition function, just define new begin/end value for _tween.
void setNewPosition() {
_tween.begin = 0; //new begin int value
_tween.end = 1; //new end int value
_animationController.reverse();
}
So I'm trying to create a trivial slide transition element in flutter and I'm having some difficulty. What the below does is wait for the animation time, and then just display the Text("hello there sailor"). I don't know why this is not animating - it seems very similar to this previous post that has a trivial example (Sliding animation to bottom in flutter).
This is how I call the below code: DeleteCheck(offsetBool: widget.model.deleteNotify, widthSlide: 0.50*width100) where double width100 = MediaQuery.of(context).size.width;.
Does anyone see what I am doing wrong?
class DeleteCheck extends StatefulWidget{
final offsetBool;
final double widthSlide;
DeleteCheck({
Key key,
this.offsetBool,
this.widthSlide
}): super(key: key);
#override
State<StatefulWidget> createState() {
return new _MyDeleteCheck();
}
}
class _MyDeleteCheck extends State<DeleteCheck> with TickerProviderStateMixin {
AnimationController _controller;
Animation<Offset> _offsetFloat;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_offsetFloat = Tween<Offset>(begin: Offset(widget.widthSlide, 0.0), end: Offset.zero)
.animate(_controller);
_offsetFloat.addListener((){
setState((){});
});
_controller.forward();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
double height100 = MediaQuery.of(context).size.height;
double width100 = MediaQuery.of(context).size.width;
return new SlideTransition(
position: _offsetFloat,
child: Container(
color: Colors.cyan,
width: 0.525*width100-3.0,
child: Text("hello there sailor")
)
);
}
}
I have good news for you! Your code is working! :) The animation looks like it is not happening, because the distance it moves is huge. The Offset, passed to the SlideTransition, is relative to its childs size. For example your child has width: 100.0 and you offset with Offset(2.0, 0.0), your child will have moved 200.0 pixels to the right.
Just try to
change begin: Offset(widget.widthSlide, 0.0), end: Offset.zero
to begin: Offset(2.0, 0.0), end: Offset.zero. You'll see the text slowly animating from the right to the center of the screen. Therefore you just need to adjust your parameterisation.
Anyway here are some additional suggestions for optimizing your code:
If you are using prebuilt AnimatedWidgets like the SlideTransition, you do not need to call addListener with setState on the controller. The AnimatedWidget takes care of it by itself. Hence you can remove the follwing lines:
lines:
_offsetFloat.addListener((){
setState((){});
});
Also it is not necessary to call const constructors. You can just leave this keyword out like new. The compiler will optimize and choose the right constructor in each case.
I have the following code in place to animate a Positioned widgets, position, when a pan ends. However, it doesn't animate.
Initialising AnimationController and variables:
AnimationController nodesListAnimationController;
Animation<double> nodesListAnimation;
double nodesListOffsetY = 0.0; //widget default offset (will be large, but acts like 0.0)
double nodesListDragOffsetY = 0.0; //how far we have moved the widget from default offset (nodesListOffsetY)
double nodesListTouchOffset = 0.0; //the initial touch when drag begins
final double nodesListHeight = 350.0; //widget height
#override
void initState() {
super.initState();
nodesListAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
)
..addListener((){
setState(() {
});
});
}
Build function; See the Positioned widget. I can drag this up and down within the bound I have created using the panning callbacks on GestureDetector.
When the pan ends, I want to animate the Positioned widget back to a given position. Currently, no animation occurs.
#override
Widget build(BuildContext context) {
nodesListOffsetY = MediaQuery.of(context).size.width + 110.0;
AppBar appBar = AppBar(
...
);
return Scaffold(
appBar: appBar,
backgroundColor: Colors.black,
body: Stack(
children: <Widget>[
Column(
children: <Widget>[
cameraWidget(),
cameraControls(),
],
),
Positioned(
left: 0.0,
top: nodesListAnimationController.isAnimating ? nodesListAnimation.value : nodesListOffsetY + nodesListDragOffsetY,
width: MediaQuery.of(context).size.width,
height: nodesListHeight,
child: GestureDetector(
onPanStart: (dragDetails) {
nodesListTouchOffset = (dragDetails.globalPosition.dy - appBar.preferredSize.height) - nodesListOffsetY - nodesListDragOffsetY;
},
onPanUpdate: (dragDetails) {
setState(() {
double newDragOffset = (dragDetails.globalPosition.dy - nodesListOffsetY) - appBar.preferredSize.height - nodesListTouchOffset;
//allow it only to move up if is clips off screen, otherwise upper limit not needed
double nodesListVisible = ((MediaQuery.of(context).size.height - appBar.preferredSize.height) - nodesListOffsetY);
bool isClipping = nodesListVisible < nodesListHeight ? true : false;
double upperLimit = 0.0;
if (isClipping) upperLimit = -(nodesListHeight - nodesListVisible);
//limit drag bounds. don't drag too high or low.
if (newDragOffset < upperLimit) newDragOffset = upperLimit;
else if (newDragOffset > 0) newDragOffset = 0.0;
else nodesListDragOffsetY = newDragOffset;
});
},
onPanEnd: (dragDetails) {
double currentPos = (nodesListOffsetY + nodesListDragOffsetY);
nodesListAnimation = Tween(begin: currentPos, end: nodesListOffsetY).animate(nodesListAnimationController);
nodesListAnimationController.forward(from: currentPos);
nodesListAnimation.addStatusListener((state) {
print(nodesListAnimation.value);
});
},
child: nodesList(),
),
),
],
));
}
Late to the party, but your issue is because of:
nodesListAnimationController.forward(from: currentPos)
Whose from is incorrectly accepting a value outside of 0.0-1.0. So you should probably change it to just controller.forward()
This isn’t well documented, but forward’s from is meant to accept values from 0.0-1.0, or more specifically, if set, then the range provided by lowerBound and upperBound (with few exceptions). (you can also think of it as 0-100%). It is not meant to accept the value range from the Tween’s begin and end, nor is it aware of that range.
https://api.flutter.dev/flutter/animation/AnimationController-class.html
By default, an AnimationController linearly produces values that range from 0.0 to 1.0
https://flutter.dev/docs/development/ui/animations/overview#addstatuslistener
An animation might then run forward (e.g., from 0.0 to 1.0)
(And I can verify that after I changed your code to use just .forward(), I was able to see it animate)