SlideAnimation Not Starting Again After Calling SetState() - flutter

I made a simple class for animating a widget into the screen:
class CustomSlideAnimation extends StatefulWidget {
static const String FROM_LEFT = 'FROM_LEFT';
static const String FROM_RIGHT = 'FROM_RIGHT';
final String from;
final Widget child;
const CustomSlideAnimation(
{Key key, this.from = FROM_LEFT, #required this.child})
: super(key: key);
#override
_CustomSlideAnimationState createState() => _CustomSlideAnimationState();
}
class _CustomSlideAnimationState extends State<CustomSlideAnimation>
with SingleTickerProviderStateMixin {
Animation<Offset> _offsetAnimation;
AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_offsetAnimation = widget.from == CustomSlideAnimation.FROM_LEFT
? Tween<Offset>(
begin: Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
curve: Curves.linear,
parent: _animationController,
))
: Tween<Offset>(
begin: Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
curve: Curves.linear,
parent: _animationController,
));
_animationController.forward();
}
#override
void dispose() {
super.dispose();
_animationController.dispose();
}
#override
Widget build(BuildContext context) {
return SlideTransition(
position: _offsetAnimation,
child: widget.child,
);
}
}
And a simple app to demonstrate the problem:
void main() => runApp(Test());
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
bool widget1 = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: widget1
? CustomSlideAnimation(
child: InkWell(
child: Text('widget1'),
onTap: () {
setState(() {
widget1 = false;
});
},
),
from: CustomSlideAnimation.FROM_LEFT,
)
: CustomSlideAnimation(
child: InkWell(
child: Text('widget2'),
onTap: () {
setState(() {
widget1 = true;
});
},
),
from: CustomSlideAnimation.FROM_LEFT,
),
),
),
);
}
}
When I start the app for first time the Text widget as expected slides in to the screen,
but then I tap it and thus calling setState() the second Text widget just appears suddenly into the screen and no slide transition happens.
I printed the times when build get called in all the classes and I see that setState() is triggering builds in Test and CustomSlideTransition classes but not triggering initState() in the CustomSlideTransition.
EDIT for more clarification: even putting controller.forward() inside the build() method didn't work.
So what am I missing?

You just need to add a UniqueKey() to every CustomSlideAnimation() to tell flutter to treat them like different widgets.
void main() => runApp(Test());
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
bool widget1 = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: widget1
? CustomSlideAnimation(
key: UniqueKey(),
child: InkWell(
child: Text('widget1'),
onTap: () {
setState(() {
widget1 = false;
});
},
),
from: CustomSlideAnimation.FROM_LEFT,
)
: CustomSlideAnimation(
key: UniqueKey(),
child: InkWell(
child: Text('widget2'),
onTap: () {
setState(() {
widget1 = true;
});
},
),
from: CustomSlideAnimation.FROM_LEFT,
),
),
),
);
}
}
Also, keep controller.forward() inside initState()

I figured it out, the reason is in the keys and widget trees as explained in this video.
When the setState was called without CustomSlideTransition having a unique key, then the framework did not recreate the State object of this class again and it instead used the State object of the previous widget which had the AnimationStatus as completed
But now with keys each time the framework creates a new State object and calls initState() where the controller is reset and the animation starts normally.
I also found a workaround but I don't know if it is as efficient as the "keys" solution:
Without adding a key to the CustomSlideTransition widget,I can, in the build() method of this widget, do these two commands:
_animationController.reset();
_animationController.forward();
and remove the call to _animationController.forward(); in initState().

Related

How can use AnimationController in Cubit or Bloc?

I want to use AnimationController in the StatelessWidget class with Cubit or Bloc in a flutter, if anyone can help me with an example link to explain?
I think the general practice is to avoid using presentation layer components in Blocs. I would use Bloc to manage my state value, but run the animation components in a stateful widget.
Blocs
part 'side_bloc.freezed.dart';
typedef StateEmitter = Emitter<SideState>;
class SideBloc extends Bloc<SideEvent, SideState> {
SideBloc() : super(const SideState(20)) {
on<SideIncrement>(onIncrement);
on<SideDecrement>(onDecrement);
}
void onIncrement(SideIncrement event, StateEmitter emit) {
emit(state.copyWith(side: state.side + 10));
}
void onDecrement(SideDecrement event, StateEmitter emit) {
if (state.side <= 10) {
return;
}
emit(state.copyWith(side: state.side - 10));
}
}
#freezed
class SideEvent with _$SideEvent {
const factory SideEvent.increment() = SideIncrement;
const factory SideEvent.decrement() = SideDecrement;
}
#freezed
class SideState with _$SideState {
const factory SideState(int side) = _SideState;
}
Animated component
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<int> _animation;
late CurvedAnimation _curvedAnimation;
#override
void initState() {
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
);
_animation = IntTween(begin: 20, end: 20).animate(_curvedAnimation);
_controller.forward();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _animateTo(int value) {
int old = _animation.value;
_controller.reset();
_animation = IntTween(begin: old, end: value).animate(
_curvedAnimation,
);
_controller.forward();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: BlocListener<SideBloc, SideState>(
listener: (context, state) {
_animateTo(state.side);
},
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return SizedSquare(side: _animation.value);
},
),
),
floatingActionButton: const SideChangeButtons(),
);
}
}
class SideChangeButtons extends StatelessWidget {
const SideChangeButtons({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () =>
context.read<SideBloc>().add(const SideEvent.increment()),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: () =>
context.read<SideBloc>().add(const SideEvent.decrement()),
child: const Icon(Icons.remove),
),
],
);
}
}
class SizedSquare extends StatelessWidget {
final int side;
const SizedSquare({
Key? key,
required this.side,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: SizedBox.fromSize(
size: Size.square(side.toDouble()),
child: Container(color: Colors.red),
),
);
}
}
AnimationController works with TickerProviderStateMixin which means that you MUST use a statefulWidget somewhere in your widget tree.

How Animate Like Button in Flutter

I would like to ask you how to animate the size of an icon when you click on an image like on Instagram, Tiktok...
This is what I tried (and many other things) but without success.
GestureDetector(
onDoubleTap: (){
setState(() {
_showLikeAnimation = true;
_sizeFavoriteAnimation = 60.0; //old value is 20.0
});
},
child: Stack(
alignment: AlignmentDirectional.center,
children: [
_imagePost(),
AnimatedSize(curve: Curves.easeIn, duration: const Duration(seconds: 2), child: Icon(Icons.favorite, size: _sizeFavoriteAnimation))
],
)),
Would you have an idea?
One way you can achieve this is by using ScaleTransition and a CurvedAnimation. Below is a simple example.
When the user taps the icon, I change the look of the icon so it shows the latest state (active/not active) and I make it a little smaller. When this transition ends I make the icon big again. This is similar to how a button behaves in the real world when you press it. I hope this is what you had in mind.
class LikeButton extends StatefulWidget {
const LikeButton({Key? key}) : super(key: key);
#override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 200), vsync: this, value: 1.0);
bool _isFavorite = false;
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_isFavorite = !_isFavorite;
});
_controller
.reverse()
.then((value) => _controller.forward());
},
child: ScaleTransition(
scale: Tween(begin: 0.7, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut)),
child: _isFavorite
? const Icon(
Icons.favorite,
size: 30,
color: Colors.red,
)
: const Icon(
Icons.favorite_border,
size: 30,
),
),
);
}
}
You can use https://pub.dev/packages/lottie to animate,
try this;
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Material App', home: LottieLearn());
}
}
class LottieLearn extends StatefulWidget {
const LottieLearn({Key? key}) : super(key: key);
#override
State<LottieLearn> createState() => _LottieLearnState();
}
class _LottieLearnState extends State<LottieLearn> with TickerProviderStateMixin {
#override
Widget build(BuildContext context) {
final AnimationController darkmodeController =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
final AnimationController favoriteController =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
bool isLight = false;
bool isFavorite = false;
const String favoriteButton = "https://assets10.lottiefiles.com/packages/lf20_slDcnv.json";
const String darkMode = "https://assets10.lottiefiles.com/packages/lf20_tbyegho2.json";
return Scaffold(
appBar: AppBar(
actions: [
InkWell(
onTap: () async {
await darkmodeController.animateTo(isLight ? 0.5 : 1);
// controller.animateTo(0.5);
isLight = !isLight;
},
child: Lottie.network(darkMode, repeat: false, controller: darkmodeController)),
InkWell(
onTap: () async {
await favoriteController.animateTo(isFavorite ? 1 : 0);
isFavorite = !isFavorite;
},
child: Lottie.network(favoriteButton, repeat: false, controller: favoriteController)),
],
),
);
}
}
this must give you an idea, just "flutter pub add lottie" then copy paste code

Rotation Transition to rotate anticlockwise flutter

I have this code that makes the icon rotate clockwise. I want to make it rotate anticlockwise. how can I do that?
animationController =
AnimationController(vsync: this, duration: Duration(seconds: 3))
..repeat(reverse: true);
RotationTransition(
turns: animationController,
child: Icon(Icons.settings_sharp),
alignment: Alignment.center,
)
Concerning RotationTransition widget rotation direction is manipulated by turns property.
When widget rotates clockwise controllers value goes from 0.0 to 1.0.
To rotate in opposite direction you need value to go from 1.0. to 0.0.
To achieve that, you can set up Tween:
final Tween<double> turnsTween = Tween<double>(
begin: 1,
end: 0,
);
And animate this tween in turns property with any AnimationController you need:
child: RotationTransition(
turns: turnsTween.animate(_controller),
...
Sample result and code to reproduce:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
/// AnimationControllers can be created with `vsync: this` because of TickerProviderStateMixin.
class _MyStatefulWidgetState extends State<MyStatefulWidget>
with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(seconds: 4),
vsync: this,
)..repeat();
final Tween<double> turnsTween = Tween<double>(
begin: 1,
end: 0,
);
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RotationTransition(
turns: turnsTween.animate(_controller),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: FlutterLogo(size: 150.0),
),
),
),
);
}
}
You can add in an AnimationBuilder and Animation object and set the end Tween property to negative.
Also, It looked like you were attempting to have the widget reverse rotation upon completion in a cycle? If so, I added in the code for that too if it helps :)
For example:
#override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 3))
//To have the widget change rotation direction upon completion
..addStatusListener((status) {
if(status == AnimationStatus.completed)
_controller.reverse();
if(status == AnimationStatus.dismissed){
_controller.forward();
}
});
_animTurn = Tween<double>(begin: 0.0, end: -1.0).animate(_controller);
_controller.forward();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return
RotationTransition(
turns: _animTurn,
child: Icon(Icons.settings_sharp,size: 100,),
alignment: Alignment.center,
);
});
}
}

How to start same animation multiple times concurrently

I am currently working on a hit/damage animation in Flutter. I want that each time the screen is tapped, it throws an animation showing an integer. I could not find a way to make it work. For now each time I tap the screen, the animation starts over stopping the previous one. I use the BLoC pattern inside the project so this animation is thrown by a streambuilder.
Here is my current code:
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final StreamController<int> streamController = StreamController<int>();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: GestureDetector(
onTap: () {
streamController.sink.add(Random().nextInt(5));
},
child: Scaffold(
body: Center(
child: StreamBuilder<int>(
stream: streamController.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return DamageAnimated(snapshot.data);
}
return Container();
},
),
),
),
),
);
}
}
class DamageAnimated extends StatefulWidget {
const DamageAnimated(this.damage);
final int damage;
#override
_DamageAnimatedState createState() => _DamageAnimatedState();
}
class _DamageAnimatedState extends State<DamageAnimated> with SingleTickerProviderStateMixin {
AnimationController animationController;
#override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
animationController.forward(from: 0.0);
return AnimatedBuilder(
animation: animationController,
builder: (BuildContext context, Widget child) {
return Transform.translate(
offset: Offset(0, -100 * animationController.value),
child: Opacity(
opacity: 1 - animationController.value,
child: Text(
'${widget.damage}',
),
),j
);
},
);
}
}
This displays an integer translating upward and fading away at same time but I can't figure out how to have the same animation running multiple time concurrently.
You're only ever returning 1 DamageAnimated. Look at the example below - you'll need to implement something similar. Btw, the ... syntax to expand a list is new in dart, so you need to update sdk version in pubspec.yaml
environment:
sdk: ">=2.2.2 <3.0.0"
I've tested this on iOS, not Android, but no reason it shouldn't work.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Widget> _anims = [];
int _count = 0;
int _animationsRunning = 0;
void animationEnded() {
_animationsRunning--;
if (_animationsRunning == 0) {
setState(() {
_anims = [];
});
print('all animations completed - removing widget from stack (now has ${_anims.length} elements)');
}
}
void _startAnimation() {
setState(() {
_anims.add(DamageAnimated(_count, animationEnded));
_count++;
_animationsRunning++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Stack(
children: <Widget>[
..._anims,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _startAnimation,
tooltip: 'Start animation',
child: Icon(Icons.add),
),
);
}
}
class DamageAnimated extends StatefulWidget {
const DamageAnimated(this.damage, this.endedCallback);
final int damage;
final VoidCallback endedCallback;
#override
_DamageAnimatedState createState() => _DamageAnimatedState();
}
class _DamageAnimatedState extends State<DamageAnimated> with SingleTickerProviderStateMixin {
AnimationController animationController;
#override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed)
if (mounted) {
widget.endedCallback();
}
});
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
animationController.forward(from: 0.0);
return AnimatedBuilder(
animation: animationController,
builder: (BuildContext context, Widget child) {
return Transform.translate(
offset: Offset(0, -100 * animationController.value),
child: Opacity(
opacity: 1 - animationController.value,
child: Text(
'${widget.damage}',
),
),
);
},
);
}
}
Maybe you could try adding StatusListeners to the animation or you could, instead, reset and start the animation each time you tap the screen such as:
onTap: () {
streamController.sink.add(Random().nextInt(5));
animationController.reset();
animationController.forward(from: 0.0);
},
I couldn't try this on my own at the moment, but I think it's a way to do it.
Sorry for waisting your time if it doesn't work.

Controlling animation from other widget

I'm trying to control an animation on a widget (widget 2 in the pic) using gestures captured from another widget (widget 1 in the pic) and I'm having trouble finding the right way to do that.
The approach I currently think about is having the AnimationController in a common parent. widget 1 would, via callbacks, update the value of this controller in the parent. widget 2 would receive that controller a a parameter and therefore the animation in widget 2 would correspond to the values provided by the gestures in widget 1
Is that the right way to do it ? If not, what would be a better approach ?
Edit: After trying this method, I notice that an exception get thrown
Another exception was thrown: AnimationController.dispose() called more than once.
I suspect the AnimationController object to be reconstructed on rebuild causing the error. Any idea on how to do it to avoid that ?
Thanks ! :)
Here is a full sample of what I think you're trying to achieve, using AnimatedWidget. Might not be the most efficient way to do it, but it works.
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Test")),
body: SafeArea(child: Parent()),
),
);
}
}
class Parent extends StatefulWidget {
#override
_ParentState createState() => _ParentState();
}
class _ParentState extends State<Parent> with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<double> _animation;
#override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this);
_animation =
Tween<double>(begin: 0, end: 100).animate(_animationController);
}
#override
void dispose() {
super.dispose();
_animationController.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(child: Widget1(animation: _animation)),
Widget2(onSliderChanged: (value) {
_animationController.value = value / 100;
}),
],
);
}
}
class Widget1 extends AnimatedWidget {
Widget1({Key key, #required Animation<double> animation})
: super(key: key, listenable: animation);
#override
Widget build(BuildContext context) {
Animation<double> animation = listenable;
return Opacity(
opacity: animation.value / 100,
child: Center(
child: Text(
"Hi",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 40.0),
),
),
);
}
}
class Widget2 extends StatefulWidget {
final ValueChanged<double> onSliderChanged;
const Widget2({Key key, this.onSliderChanged}) : super(key: key);
#override
State<StatefulWidget> createState() => _Widget2State();
}
class _Widget2State extends State<Widget2> {
double _value = 0;
#override
Widget build(BuildContext context) {
return Slider(
value: _value,
min: 0,
max: 100,
onChanged: (value) {
setState(() {
_value = value;
});
widget.onSliderChanged(value);
},
);
}
}