Listen for an animation to complete - animation

I'm trying to perform an action after my animation finishes. I tried adding a statusListener but that is not working for me. My code looks like this:
#override
void initState() {
super.initState();
_controller = new AnimationController(
duration: new Duration(milliseconds: 500),
vsync: this,
)..addStatusListener((AnimationStatus status) {
print("Going");
if (status.index == 3 && spins > 0) { // AnimationStatus index 3 == completed animation
_controller.duration = new Duration(milliseconds: speed - 50);
_controller.forward(from: _controller.value == null ? 0.0 : 1 - _controller.value);
spins--;
print(speed);
}
});
}
The print(Going); never gets executed but my animation does end. What is going wrong?
///---Edit---///
I'm using an AnimatedBuilder, that part of the code looks like this:
child: new AnimatedBuilder(
animation: _controller,
child: new Image.network(widget.url),
builder: (BuildContext context, Widget child) {
return new Transform.rotate(
angle: _controller.value * 2.0 * math.PI,
child: child,
);
},
),

Reacting to your comment and edit I looked into the AnimationBuilder. Adapting the example in the docs I came up with this working solution:
class Spinner extends StatefulWidget {
#override
_SpinnerState createState() => new _SpinnerState();
}
class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
AnimationController _controller;
CurvedAnimation _animation;
#override
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(seconds: 5),
vsync: this,
)..forward();
_animation = new CurvedAnimation(
parent: _controller,
curve: Curves.linear,
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed)
print('completed');
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new AnimatedBuilder(
animation: _animation,
child: new Container(width: 200.0, height: 200.0, color: Colors.green),
builder: (BuildContext context, Widget child) {
return new Transform.rotate(
angle: _controller.value * 2.0 * 3.1415,
child: child,
);
},
);
}
}
As you can see, I used the controller as parent to an animation, which was than used as animation for the AnimationBuilder. Hope it helps.

You can also use the whenComplete listener
_animationController.forward().whenComplete((){
//Trigger different responses, when animation is started at different places.
})

Following the example in the flutter gallery's progress indicators you should attach the StatusListener to an animation, not the controller
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..forward();
_animation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.9, curve: Curves.fastOutSlowIn),
reverseCurve: Curves.fastOutSlowIn
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed)
_controller.forward();
else if (status == AnimationStatus.completed)
_controller.reverse();
});
I have not tested this with your code. Just shout, if it doesn't work ;)

Related

How to create a Slide-In and Slide-Out animation in Flutter?

I am trying to create a Slide in and Slide out animation in Flutter. Animation should look like this:
----- Widget slides in ---> Wait for 1 seconds -----Widget slides out of screen -->
I have tried following code but my animation is stuck in a loop.
class _MyStatefulWidgetState extends State<MyStatefulWidget> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Offset> _positionAnimation;
Animation<double> opacityAnimation;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_positionAnimation = Tween<Offset>(
begin: const Offset(-1, 0),
end: const Offset(0, 0.0),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut),
)..addStatusListener((status) {
print('animation 1 status $status');
if (status == AnimationStatus.completed) {
_controller.reset();
}
if (status == AnimationStatus.dismissed) {
_positionAnimation = Tween<Offset>(
begin: const Offset(0, 0),
end: const Offset(1, 0.0),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.elasticIn),
)..addStatusListener((status2) {
print('animation 2 status $status2');
if (status == AnimationStatus.dismissed) {
_controller.reset();
}
});
_controller.forward();
}
});
_controller.forward();
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
builder: _buildAnimation,
animation: _controller,
);
}
Widget _buildAnimation(BuildContext context, Widget child) {
return Opacity(
opacity: 1,
child: SlideTransition(
position: _positionAnimation,
child: Container(
color: Colors.blueAccent,
height: 100,
child: Center(
child: Text(
'Hello, World!',
style: Theme.of(context).textTheme.display1,
),
),
),
),
);
}
With this approach animation is stuck in a loop.
I cannot use staggered animation as I am trying to animate same property. (Or is there a way to use staggered animation on same property?).
Any better way for implementing this?
Taking idea from #pskink code in comments, achieved desired effect with following code using TweenAnimationBuilder:
class _SlideInOutWidgetState extends State<SlideInOutWidget>
with SingleTickerProviderStateMixin {
double startPos = -1.0;
double endPos = 0.0;
Curve curve = Curves.elasticOut;
#override
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: Tween<Offset>(begin: Offset(startPos, 0), end: Offset(endPos, 0)),
duration: Duration(seconds: 1),
curve: curve,
builder: (context, offset, child) {
return FractionalTranslation(
translation: offset,
child: Container(
width: double.infinity,
child: Center(
child: child,
),
),
);
},
child: Text('animated text', textScaleFactor: 3.0,),
onEnd: () {
print('onEnd');
Future.delayed(
Duration(milliseconds: 500),
() {
curve = curve == Curves.elasticOut
? Curves.elasticIn
: Curves.elasticOut;
if (startPos == -1) {
setState(() {
startPos = 0.0;
endPos = 1.0;
});
}
},
);
},
);
}
}
Use Marquee plugin for text Animations
Install :
marquee: ^1.3.1
Example :
Marquee(
text: 'There once was a boy who told this story about a boy: "',
)
Fore more info try marquee | flutterpckage

How to shake a widget in Flutter on invalid input?

On my signup form, I have a checkbox which needs to shake a bit whenever the user tries to login before accepting the terms and conditions. How can I achieve something like this Flutter?
import 'package:flutter/material.dart';
#immutable
class ShakeWidget extends StatelessWidget {
final Duration duration;
final double deltaX;
final Widget child;
final Curve curve;
const ShakeWidget({
Key key,
this.duration = const Duration(milliseconds: 500),
this.deltaX = 20,
this.curve = Curves.bounceOut,
this.child,
}) : super(key: key);
/// convert 0-1 to 0-1-0
double shake(double animation) =>
2 * (0.5 - (0.5 - curve.transform(animation)).abs());
#override
Widget build(BuildContext context) {
return TweenAnimationBuilder<double>(
key: key,
tween: Tween(begin: 0.0, end: 1.0),
duration: duration,
builder: (context, animation, child) => Transform.translate(
offset: Offset(deltaX * shake(animation), 0),
child: child,
),
child: child,
);
}
}
If you need to re-enable shaking, just change ShakeWidget key to some random one.
I achieved this a different way because I wanted to be able to control the duration and get a bit more vigorous shaking. I also wanted to be able to add this easily as a wrapper for other child widgets so I looked up how to use keys to have a parent control actions in a child widget. Here is the class:
class ShakerState extends State<Shaker> with SingleTickerProviderStateMixin {
late AnimationController animationController;
late Animation<double> animation;
#override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 800), // how long the shake happens
)..addListener(() => setState(() {}));
animation = Tween<double>(
begin: 00.0,
end: 120.0,
).animate(animationController);
}
math.Vector3 _shake() {
double progress = animationController.value;
double offset = sin(progress * pi * 10.0); // change 10 to make it vibrate faster
return math.Vector3(offset * 25, 0.0, 0.0); // change 25 to make it vibrate wider
}
shake() {
animationController.forward(from:0);
}
#override
Widget build(BuildContext context) {
return Transform(
transform: Matrix4.translation(_shake()),
child: widget.child,
);
}
}
And then to use this you need a key in your parent:
final GlobalKey<ShakerState> _shakeKey = GlobalKey<ShakerState>();
And then you can do something like this inside your parent body (see where "Shaker" is used around the child I want to shake):
...
Container(
height: 50,
width: 250,
decoration: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(20)),
child: TextButton(
onPressed: () => _handleEmailSignIn(loginController.text, loginPasswordController.text),
child: Shaker(_shakeKey, Text('Login', // <<================
style: TextStyle(color: Colors.white, fontSize: 25),
)),
),
),
...
Then with the controller you can trigger the shake at a time you want programmatically like this (see the use of "_shakeKey"):
Future<void> _handleEmailSignIn(String user, password) async {
try {
await auth.signInWithEmailAndPassword(email: user, password: password);
FocusScope.of(context).unfocus();
await Navigator.pushNamedAndRemoveUntil(context, '/next_page', ModalRoute.withName('/'));
} on FirebaseAuthException catch (e) {
_shakeKey.currentState?.shake(); // <<=============
if (e.code == 'user-not-found') {
print('No user found for that email.');
} else if (e.code == 'wrong-password') {
print('Wrong password provided for that user.');
}
}
setState(() {});
}
Little improved of #Kent code (added controller).
import 'package:flutter/material.dart';
class ShakeX extends StatefulWidget {
final Widget child;
final double horizontalPadding;
final double animationRange;
final ShakeXController controller;
final Duration animationDuration;
const ShakeX(
{Key key,
#required this.child,
this.horizontalPadding = 30,
this.animationRange = 24,
this.controller,
this.animationDuration = const Duration(milliseconds: 500)})
: super(key: key);
#override
_ShakeXState createState() => _ShakeXState();
}
class _ShakeXState extends State<ShakeX> with SingleTickerProviderStateMixin {
AnimationController animationController;
#override
void initState() {
animationController =
AnimationController(duration: widget.animationDuration, vsync: this);
if (widget.controller != null) {
widget.controller.setState(this);
}
super.initState();
}
#override
Widget build(BuildContext context) {
final Animation<double> offsetAnimation =
Tween(begin: 0.0, end: widget.animationRange)
.chain(CurveTween(curve: Curves.elasticIn))
.animate(animationController)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
animationController.reverse();
}
});
return AnimatedBuilder(
animation: offsetAnimation,
builder: (context, child) {
return Container(
margin: EdgeInsets.symmetric(horizontal: widget.animationRange),
padding: EdgeInsets.only(
left: offsetAnimation.value + widget.horizontalPadding,
right: widget.horizontalPadding - offsetAnimation.value),
child: widget.child,
);
},
);
}
}
class ShakeXController {
_ShakeXState _state;
void setState(_ShakeXState state) {
_state = state;
}
Future<void> shake() {
print('shake');
return _state.animationController.forward(from: 0.0);
}
}
import 'package:flutter/material.dart';
class ShakeError extends StatefulWidget {
const ShakeError({
Key? key,
required this.child,
this.controller,
this.duration = const Duration(milliseconds: 500),
this.deltaX = 20,
this.curve = Curves.bounceOut,
}) : super(key: key);
final Widget child;
final Duration duration;
final double deltaX;
final Curve curve;
final Function(AnimationController)? controller;
#override
State<ShakeError> createState() => _ShakeErrorState();
}
class _ShakeErrorState extends State<ShakeError>
with SingleTickerProviderStateMixin<ShakeError> {
late AnimationController controller;
late Animation<double> offsetAnimation;
#override
void initState() {
controller = AnimationController(duration: widget.duration, vsync: this);
offsetAnimation = Tween<double>(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: widget.curve))
.animate(controller);
if (widget.controller is Function) {
widget.controller!(controller);
}
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
/// convert 0-1 to 0-1-0
double shake(double animation) =>
2 * (0.5 - (0.5 - widget.curve.transform(animation)).abs());
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: offsetAnimation,
builder: (BuildContext context, Widget? child) {
return Transform.translate(
offset: Offset(widget.deltaX * shake(offsetAnimation.value), 0),
child: child,
);
},
child: widget.child,
);
}
}
Here is some code from my app. It shakes a red x on the screen. redx.png. I'm sure you could adopt it to your use case. I'm using AnimatedBuilder.
Code in action:
https://giphy.com/gifs/Yo2u06oMu1ksPYRD3B
import 'package:flutter/material.dart';
class ShakeX extends StatefulWidget {
const ShakeX({
Key key,
}) : super(key: key);
#override
_ShakeXState createState() => _ShakeXState();
}
class _ShakeXState extends State<ShakeX> with SingleTickerProviderStateMixin{
AnimationController controller;
#override
void initState() {
controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) {
final Animation<double> offsetAnimation =
Tween(begin: 0.0, end: 24.0).chain(CurveTween(curve: Curves.elasticIn)).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
}
});
controller.forward(from: 0.0);
return AnimatedBuilder(animation: offsetAnimation,
builder: (context, child){
if (offsetAnimation.value < 0.0) print('${offsetAnimation.value + 8.0}');
return Container(
margin: EdgeInsets.symmetric(horizontal: 24.0),
padding: EdgeInsets.only(left: offsetAnimation.value + 30.0, right: 30.0 - offsetAnimation.value),
child: Image.asset("assets/redx.png"),
);
},);
}
}
For those still looking... You can achieve something like that using Animated Widget
ShakeAnimatedWidget(
enabled: this._enabled,
duration: Duration(milliseconds: 1500),
shakeAngle: Rotation.deg(z: 40),
curve: Curves.linear,
child: FlutterLogo(
style: FlutterLogoStyle.stacked,
),
),
Check the link for even advance usage.
There are many animation based packages in flutter, you can check that site (https://fluttergems.dev/animation-transition/) to see them. As developer, you don't have to to create animation classes from scratch.
Regarding shaking animation, I would suggest flutter_animator package. There is a shake widget which exactly performs what you need.
My version on previous code. It prevents usage of paddings and margins
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ShakeX extends StatefulWidget {
final Widget child;
final double horizontalPadding;
final double animationRange;
final ShakeXController shakeXController;
final Duration animationDuration;
const ShakeX({
Key? key,
required this.child,
this.horizontalPadding = 16,
this.animationRange = 16,
required this.shakeXController,
this.animationDuration = const Duration(milliseconds: 500),
}) : super(key: key);
#override
ShakeXState createState() => ShakeXState();
}
class ShakeXState extends State<ShakeX> with SingleTickerProviderStateMixin {
late AnimationController animationController;
var childSize = Size.zero;
#override
void initState() {
super.initState();
animationController = AnimationController(duration: widget.animationDuration, vsync: this);
widget.shakeXController.setState(this);
}
#override
Widget build(BuildContext context) {
final Animation<double> offsetAnimation = Tween(begin: 0.0, end: widget.animationRange)
.chain(CurveTween(curve: Curves.elasticIn))
.animate(animationController)
..addStatusListener((status) {
if (status == AnimationStatus.completed && status != AnimationStatus.reverse) {
animationController.reverse();
}
});
return AnimatedBuilder(
animation: offsetAnimation,
builder: (context, child) {
return SizedBox(
width: childSize.width,
height: childSize.height,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: offsetAnimation.value,
child: MeasureSize(
onChange: (size) {
setState(() {
childSize = size;
});
},
child: widget.child),
),
],
),
);
},
);
}
}
class ShakeXController {
late ShakeXState _state;
void setState(ShakeXState state) {
_state = state;
}
Future<void> shake() {
return _state.animationController.forward(from: 0.0);
}
}
typedef void OnWidgetSizeChange(Size size);
class MeasureSizeRenderObject extends RenderProxyBox {
Size? oldSize;
OnWidgetSizeChange onChange;
MeasureSizeRenderObject(this.onChange);
#override
void performLayout() {
super.performLayout();
Size newSize = child!.size;
if (oldSize == newSize) return;
oldSize = newSize;
WidgetsBinding.instance.addPostFrameCallback((_) {
onChange(newSize);
});
}
}
class MeasureSize extends SingleChildRenderObjectWidget {
final OnWidgetSizeChange onChange;
const MeasureSize({
Key? key,
required this.onChange,
required Widget child,
}) : super(key: key, child: child);
#override
RenderObject createRenderObject(BuildContext context) {
return MeasureSizeRenderObject(onChange);
}
#override
void updateRenderObject(BuildContext context, covariant MeasureSizeRenderObject renderObject) {
renderObject.onChange = onChange;
}
}

Difference between Animation Tween and Animation Controlller

In some Flutter animation tutorials, some uses a Tween and an Animation object. Some uses AnimationController only. Both code below seems to output the same result. So when do we use a Tween with animation and when do we use AnimationController only?
With Tween and animation
import 'dart:core';
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
#override
_State createState() {
return _State();
}
}
class _State extends State<Test> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _animation;
bool faded = true;
#override
void initState() {
super.initState();
_animationController = new AnimationController(
value:0.0,
vsync: this,
upperBound: 1.0,
lowerBound: 0.0,
duration: new Duration(seconds:1),
);
_animation = Tween(begin: 0.0, end: 1.0).animate(_animationController);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
elevation: 0.5,
title: new Text(
"Testing views",
style: Theme.of(context).textTheme.title,
),
),
body: _buildBodyAnimationTest(),
// body: _buildTuto(),
);
}
Widget _buildBodyAnimationTest(){
return FadeTransition(
opacity: _animation, //here is the difference
child: InkWell(
onTap: (){
if(faded){
faded = false;
_animationController.reverse();
}else{
faded = true;
_animationController.forward();
}
},
child: new Container(
color: Colors.red,
),
),
);
}
}
Without Tween and Animation
import 'dart:core';
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
#override
_State createState() {
return _State();
}
}
class _State extends State<Test> with TickerProviderStateMixin {
AnimationController _animationController;
bool faded = true;
#override
void initState() {
super.initState();
_animationController = new AnimationController(
value:0.0,
vsync: this,
upperBound: 1.0,
lowerBound: 0.0,
duration: new Duration(seconds:1),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
elevation: 0.5,
title: new Text(
"Testing views",
style: Theme.of(context).textTheme.title,
),
),
body: _buildBodyAnimationTest(),
// body: _buildTuto(),
);
}
Widget _buildBodyAnimationTest(){
return FadeTransition(
opacity: _animationController, //here is the difference
child: InkWell(
onTap: (){
if(faded){
faded = false;
_animationController.reverse();
}else{
faded = true;
_animationController.forward();
}
},
child: new Container(
color: Colors.red,
),
),
);
}
}
Tweens are objects used to transform the output of an Animation (such as AnimationController).
With AnimationController, you typically have a 0-1 floating value. But you rarely want that.
Tweens allows to map that 0-1 to something more concrete such as red to blue, left to right, ...
Background is my tween sequence color.
Animatable<Color> background = TweenSequence<Color>(
[
TweenSequenceItem(
weight: 1.0,
tween: ColorTween(
begin: colors[_Substance.dayEndBackground],
end: colors[_Substance.dayStartBackground],
),
),
TweenSequenceItem(
weight: 1.0,
tween: ColorTween(
begin: colors[_Substance.dayStartBackground],
end: colors[_Substance.dayEndBackground],
),
),
],
);
This is my controller defined in initState() and updated in every one second:
bgUpdateController = AnimationController(
value: _currentDateTime.hour / 24,
upperBound: 1,
lowerBound: 0,
duration: const Duration(hours: 24),
vsync: this,
)..repeat();
I have used the above background and controller as AnimatedBuilder like below:
AnimatedBuilder(
animation: bgUpdateController,
builder: (context, child) {
return Scaffold(
backgroundColor: background
.chain(
CurveTween(curve: Curves.easeInOutCirc),
)
.evaluate(
AlwaysStoppedAnimation(bgUpdateController.value),
),
...
and the result of my code is:
Conclusion
AnimationController is for how long the animation would be and how to control from time, upper and lower boundary, how to control data with time, length, sequence, etc. while AnimationTween is for the range of animation with time, colour, range, sequence, etc as long the animation would be while.

How to chain multiple controller/animation?

Issue
I made an ellipse loading animation inside of flutter, but had to use a Timer on all three different controllers. (See example below...)
Is there any widget that can help chaining three different animations?
I tried using Interval Widget for multiple curves, but it did not provide a smooth transition. e.g. Interval(0.0, 0.3), Interval(0.3, 0.6), Interval(0.6, 0.9) for the animation curves.
Sample
Sample Code
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'dart:async';
void main() {
runApp(
new MaterialApp(
home: new Scaffold(
body: new CircleLoader(),
)
)
);
}
class CircleLoader extends StatefulWidget {
#override
_CircleLoaderState createState() => new _CircleLoaderState();
}
class _CircleLoaderState extends State<CircleLoader>
with TickerProviderStateMixin {
Animation<double> animation;
Animation<double> animation2;
Animation<double> animation3;
AnimationController controller;
AnimationController controller2;
AnimationController controller3;
int duration = 1000;
Widget circle = new Container(
height: 10.0,
width: 10.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[300],
),
);
#override
initState() {
super.initState();
controller = new AnimationController(
duration: new Duration(milliseconds: duration), vsync: this);
controller2 = new AnimationController(
duration: new Duration(milliseconds: duration), vsync: this);
controller3 = new AnimationController(
duration: new Duration(milliseconds: duration), vsync: this);
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeInOut);
final CurvedAnimation curve2 =
new CurvedAnimation(parent: controller2, curve: Curves.easeInOut);
final CurvedAnimation curve3 =
new CurvedAnimation(parent: controller3, curve: Curves.easeInOut);
animation = new Tween(begin: 0.85, end: 1.5).animate(curve)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
animation2 = new Tween(begin: 0.85, end: 1.5).animate(curve2)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller2.reverse();
} else if (status == AnimationStatus.dismissed) {
controller2.forward();
}
});
animation3 = new Tween(begin: 0.85, end: 1.5).animate(curve3)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller3.reverse();
} else if (status == AnimationStatus.dismissed) {
controller3.forward();
}
});
controller.forward();
new Timer(const Duration(milliseconds: 300), () {
controller2.forward();
});
new Timer(const Duration(milliseconds: 600), () {
controller3.forward();
});
}
#override
Widget build(BuildContext context) {
return new Center(
child: new Container(
width: 100.0,
height: 50.0,
color: Colors.grey,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new ScaleTransition(scale: animation, child: circle),
new ScaleTransition(scale: animation2, child: circle),
new ScaleTransition(scale: animation3, child: circle),
],
),
),
);
}
}
The trick is that you can create your own tween.
In short, what you want is a smooth curve that goes from 0 to 1 and then 1 to 0 smoothly. Which you can assimilate to a (sin(t * 2 * PI) + 1) / 2 where 0 <= t <= 1
And then delay that curve for each cicles.
class TestTween extends Tween<double> {
final double delay;
TestTween({double begin, double end, this.delay}) : super(begin: begin, end: end);
#override
double lerp(double t) {
return super.lerp((sin((t - delay) * 2 * PI) + 1) / 2);
}
}
Which allows to do
_controller = new AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
instead of having to add an animation listener and reverse the animation.
The end result is
class CircleLoader extends StatefulWidget {
#override
_CircleLoaderState createState() => new _CircleLoaderState();
}
class _CircleLoaderState extends State<CircleLoader>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
#override
dispose() {
_controller.dispose();
super.dispose();
}
buildCircle(double delay) {
return new ScaleTransition(
scale: new TestTween(begin: .85, end: 1.5, delay: delay)
.animate(_controller),
child: new Container(
height: 10.0,
width: 10.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[300],
),
),
);
}
#override
Widget build(BuildContext context) {
return new Center(
child: new Container(
width: 100.0,
height: 50.0,
color: Colors.grey,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
buildCircle(.0),
buildCircle(.2),
buildCircle(.4),
],
),
),
);
}
}
class TestTween extends Tween<double> {
final double delay;
TestTween({double begin, double end, this.delay})
: super(begin: begin, end: end);
#override
double lerp(double t) {
return super.lerp((sin((t - delay) * 2 * PI) + 1) / 2);
}
}

Flutter: restart SlideAnimation from current object position

I've got a SlideAnimation on an object that get's triggered every time I tap the screen. The object simply slides up atm but when I tap the screen again the animation starts from the original position of the object again. How would I capture the position the object ended in the last animation and perform the animation from there on?
I've got the following code for the creating of the SlideTransition:
new SlideTransition(
child: new Container(
child: char,
),
position: _characterPosition,
)
And this code for the actual animation:
_characterPosition = new FractionalOffsetTween(
begin: const FractionalOffset(0.0, 0.0),
end: const FractionalOffset(0.0, -0.2),
).animate(new CurvedAnimation(
parent: _characterAnimationController,
curve: Curves.easeOut,
)
);
In the simplest case you can use your _characterAnimationController. AnimationController has two related methods stop and forward. Verify current animation's state and invoke required method:
void _onTap() {
if (_characterAnimationController.isAnimating) {
_characterAnimationController.stop(canceled: false);
} else {
_characterAnimationController.forward();
}
}
To extend animation after completion you can reset _characterAnimationController state via value(or create new one) and update position. Complete example:
class Home extends StatefulWidget {
#override
State createState() => new _HomeState();
}
class _HomeState extends State<Home> with TickerProviderStateMixin {
Animation _characterPosition;
AnimationController _characterAnimationController;
FractionalOffset _beginOffset;
FractionalOffset _endOffset;
FractionalOffset _animationOffset;
void _restartAnimation() {
_characterAnimationController.value = 0.0;
_beginOffset = _endOffset;
_endOffset = _endOffset + _animationOffset;
setState(() {
_characterPosition = _generateCharacterPosition();
});
_characterAnimationController.forward();
}
#override
void initState() {
_animationOffset = const FractionalOffset(0.0, 0.15);
_beginOffset = const FractionalOffset(0.0, 0.0);
_endOffset = _animationOffset;
_characterAnimationController = new AnimationController(
duration: new Duration(seconds: 5), vsync: this);
_characterPosition = _generateCharacterPosition();
}
#override
Widget build(BuildContext context) {
return new Material(
child: new InkWell(
child: new SlideTransition(
child: new Container(
child: new Text("Hello"),
),
position: _characterPosition,
),
onTap: _onTap,
),
);
}
void _onTap() {
if (_characterAnimationController.isAnimating) {
_characterAnimationController.stop(canceled: false);
} else if (_characterAnimationController.status ==
AnimationStatus.completed) {
_restartAnimation();
} else {
_characterAnimationController.forward();
}
}
Animation _generateCharacterPosition() => new FractionalOffsetTween(
begin: _beginOffset,
end: _endOffset,
)
.animate(new CurvedAnimation(
parent: _characterAnimationController,
curve: Curves.easeOut,
));
}

Resources