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,
));
}
Related
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
I've created an animated Floating Action Button (FAB) menu:
Main FAB which spawns other FABs when clicked.
It is based on several online tutorials.
Works fine besides one major issue:
The spawned FABs don't trigger an onPressed event.
Seems like there's a problem with the combination of transform animation and the stack widget (which the FABs are children of).
Works fine if I replace the stack with a row...
Seems like the issue was addressed here, but without a proper solution:
FloatingActionButton onPressed not triggering
Here's the complete code for that FAB menu.
Just supply it a list with buttons data, similar to this one:
List<Map<String, dynamic>> _buttonsData = [
{'color': Colors.green ,'icon': Icons.stop},
{'color': Colors.blue ,'icon': Icons.subway},
{'color': Colors.green ,'icon': Icons.add},];
FancyFab2(_buttonsData, null)
The code:
import 'package:flutter/material.dart';
class FancyFab extends StatefulWidget {
FancyFab(this._buttonsData, this._onSelected);
final List<Map<String, dynamic>> _buttonsData;
final ValueChanged<int> _onSelected;
#override
createState() => FancyFabState(_buttonsData, _onSelected);
}
class FancyFabState extends State<FancyFab> with SingleTickerProviderStateMixin {
final List<Map<String, dynamic>> _buttonsData;
final ValueChanged<int> _onSelected;
// state vars
AnimationController _controller;
Animation <double> _transform;
bool _isOpened = false;
FancyFabState(this._buttonsData, this._onSelected);
#override
void initState() {
// call base
super.initState();
// _controller
_controller = AnimationController(
duration: Duration(milliseconds: 100),
vsync: this
);
_transform = Tween<double>(
begin: 0.0,
end: -64.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn
),
);
}
#override
void dispose() {
// controller
_controller.dispose();
// call base
super.dispose();
}
#override
Widget build(BuildContext context) {
return _buildFab(_controller);
}
Widget _buildFab(AnimationController controller) {
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Stack(
children: List.generate(_buttonsData.length, (index) => _buildStackWidget(context, index))
);
}
);
}
Widget _buildStackWidget(BuildContext context, int index) {
Map<String, dynamic> buttonData = _buttonsData[index];
if (index == _buttonsData.length - 1)
return _buildMenuButton(index, buttonData);
else
return _buildMenuItem(index, buttonData);
}
Widget _buildMenuItem(int index, Map<String, dynamic> buttonData) {
return Transform.translate(
offset: Offset((1 + index) * _transform.value, 0.0),
child: FloatingActionButton(
heroTag: 100 + index,
backgroundColor: buttonData['color'],
//onPressed: () => _onSelected(index),
onPressed: () => print('click'),
child: Icon(buttonData['icon']),
),
);
}
Widget _buildMenuButton(int index, Map<String, dynamic> buttonData) {
return FloatingActionButton(
heroTag: 200,
backgroundColor: buttonData['color'],
onPressed: _toggle,
child: Icon(buttonData['icon']),
);
}
void _toggle() {
print('toggle');
_isOpened = !_isOpened;
if (true == _isOpened)
_controller.forward();
else
_controller.reverse();
}
}
That's because you are moving the items outside your Stack so you can't hitTest on those positions.
I modified a few lines of your code to make it works :
Add constraints to your Stack (you can use SizedBox also) , I'm using container to set a different color of background.
Widget _buildFab(AnimationController controller) {
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Container(
width: MediaQuery.of(context).size.width,
height: 100,
color: Colors.blueGrey,
child: Stack(
children: List.generate(_buttonsData.length,
(index) => _buildStackWidget(context, index))),
);
});
}
Center your items
Widget _buildMenuItem(int index, Map<String, dynamic> buttonData) {
return Center(
child: Transform.translate(
offset: Offset((1 + index) * _transform.value, 0.0),
child: FloatingActionButton(
heroTag: 100 + index,
backgroundColor: buttonData['color'],
//onPressed: () => _onSelected(index),
onPressed: () => print('click'),
child: Icon(buttonData['icon']),
),
),
);
}
Widget _buildMenuButton(int index, Map<String, dynamic> buttonData) {
return Center(
child: FloatingActionButton(
heroTag: 200,
backgroundColor: buttonData['color'],
onPressed: _toggle,
child: Icon(buttonData['icon']),
),
);
}
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.
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);
}
}
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 ;)