How can use AnimationController in Cubit or Bloc? - flutter

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.

Related

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

Flutter Reworked question: Issue sharing states between widget with button and widget with countdown timer

I am trying since some days to connect an animated stateful child widget with a countdown timer to the parent stateful widget with the user interaction. I found this answer from Andrey on a similar question (using Tween which I do not) that already helped a lot, but I still don't get it to work. My assumption is, the child's initState could be the reason. The timer's code comes from here.
I have removed quite some code including some referenced functions/classes. This should provide a clearer picture on the logic:
In MainPageState I declare and init the _controller of the animation
In MainPageState I call the stateless widget CreateKeypad hosting among others the "go" key
When go is clicked, this event is returned to MainPageState and _controller.reverse(from: 1.0); executed
In MainPageState I call the stateful widget CountDownTimer to render the timer
In _CountDownTimerState I am not sure if my initState is correct
In _CountDownTimerState I build the animation with CustomTimerPainter from the timer code source
The animation shall render a white donut and a red, diminishing arc on top. However, I only see the white donut, not the red timer's arc. Any hint is highly appreciated.
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
final String title;
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> with TickerProviderStateMixin {
AnimationController _controller;
var answer="0", correctAnswer = true, result = 0;
#override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(seconds: 7));
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
),
child: SafeArea(
child: Container(
child: Column(
children: <Widget>[
CreateKeypad( // creates a keypad with a go button. when go is clicked, countdown shall start
prevInput: int.parse((answer != null ? answer : "0")),
updtedInput: (int val) {
setState(() => answer = val.toString());
},
goSelected: () {
setState(() {
if (answer == result.toString()) {
correctAnswer = true;
}
final problem = createProblem();
result = problem.result;
});
_controller.reverse(from: 1.0); // start the countdown animation
Future.delayed(const Duration(milliseconds: 300,),
() => setState(() => correctAnswer = true));
},
),
CountDownTimer(_controller), // show countdown timer
]
),
),
)
);
}
}
// CREATE KEYPAD - all keys but "1! and "go" removed
class CreateKeypad extends StatelessWidget {
final int prevInput;
final VoidCallback goSelected;
final Function(int) updtedInput;
CreateKeypad({#required this.prevInput, #required this.updtedInput, this.goSelected});
#override
Widget build(BuildContext context) {
return Row(
children: <Widget> [
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0, height: 80.0,
child: CupertinoButton(
child: Text("1", style: TextStyle(color: CupertinoColors.black)),
onPressed: () {
updtedInput(1);
},
),
),
),
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0, height: 80.0,
child: CupertinoButton(
child: Text("Go!", style: TextStyle(color: CupertinoColors.black)),
onPressed: () => goSelected(),
),
),
),
],
),
]
);
}
}
// CREATE COUNTDOWN https://medium.com/flutterdevs/creating-a-countdown-timer-using-animation-in-flutter-2d56d4f3f5f1
class CountDownTimer extends StatefulWidget {
CountDownTimer(this._controller);
final AnimationController _controller;
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer> with TickerProviderStateMixin {
#override
void initState() {
super.initState(); // here I have some difference to Andrey's answer because I do not use Tween
}
String get timerString {
Duration duration = widget._controller.duration * widget._controller.value;
return '${duration.inMinutes}:${(duration.inSeconds % 60)
.toString()
.padLeft(2, '0')}';
}
#override
Widget build(BuildContext context) {
return Container(
child: AnimatedBuilder(
animation: widget._controller,
builder:
(BuildContext context, Widget child) {
return CustomPaint(
painter: CustomTimerPainter( // this draws a white donut and a red diminishing arc on top
animation: widget._controller,
backgroundColor: Colors.white,
color: Colors.red,
));
},
),
);
}
}
You can copy paste run full code below
Step 1: You can put controller inside CountDownTimerState
Step 2: Use GlobalKey
CountDownTimer(key: _key)
Step 3: Call function start() inside _CountDownTimerState with _key.currentState
goSelected: () {
setState(() {
...
_controller.reverse(from: 10.0); // start the countdown animation
final _CountDownTimerState _state = _key.currentState;
_state.start();
...
class _CountDownTimerState extends State<CountDownTimer>
with TickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 7));
super
.initState(); // here I have some difference to Andrey's answer because I do not use Tween
}
...
void start() {
setState(() {
_controller.reverse(from: 1.0);
});
}
working demo
full code
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
final String title;
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> with TickerProviderStateMixin {
AnimationController _controller;
var answer = "0", correctAnswer = true, result = 0;
GlobalKey _key = GlobalKey();
#override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 7));
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
//navigationBar: CupertinoNavigationBar(),
child: SafeArea(
child: Container(
color: Colors.blue,
child: Column(children: <Widget>[
CreateKeypad(
// creates a keypad with a go button. when go is clicked, countdown shall start
prevInput: int.parse((answer != null ? answer : "0")),
updtedInput: (int val) {
setState(() => answer = val.toString());
},
goSelected: () {
setState(() {
if (answer == result.toString()) {
correctAnswer = true;
}
/*final problem = createProblem();
result = problem.result;*/
});
print("go");
_controller.reverse(from: 10.0); // start the countdown animation
final _CountDownTimerState _state = _key.currentState;
_state.start();
/* Future.delayed(
const Duration(
milliseconds: 300,
),
() => setState(() => correctAnswer = true));*/
},
),
Container(
height: 400,
width: 400,
child: CountDownTimer(key: _key)), // show countdown timer
]),
),
));
}
}
// CREATE KEYPAD - all keys but "1! and "go" removed
class CreateKeypad extends StatelessWidget {
final int prevInput;
final VoidCallback goSelected;
final Function(int) updtedInput;
CreateKeypad(
{#required this.prevInput, #required this.updtedInput, this.goSelected});
#override
Widget build(BuildContext context) {
return Row(children: <Widget>[
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0,
height: 80.0,
child: CupertinoButton(
child:
Text("1", style: TextStyle(color: CupertinoColors.black)),
onPressed: () {
updtedInput(1);
},
),
),
),
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0,
height: 80.0,
child: CupertinoButton(
child:
Text("Go!", style: TextStyle(color: CupertinoColors.black)),
onPressed: () => goSelected(),
),
),
),
],
),
]);
}
}
// CREATE COUNTDOWN https://medium.com/flutterdevs/creating-a-countdown-timer-using-animation-in-flutter-2d56d4f3f5f1
class CountDownTimer extends StatefulWidget {
CountDownTimer({Key key}) : super(key: key);
//final AnimationController _controller;
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer>
with TickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 7));
super
.initState(); // here I have some difference to Andrey's answer because I do not use Tween
}
String get timerString {
Duration duration = _controller.duration * _controller.value;
return '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
}
void start() {
setState(() {
_controller.reverse(from: 1.0);
});
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return CustomPaint(
painter: CustomTimerPainter(
// this draws a white donut and a red diminishing arc on top
animation: _controller,
backgroundColor: Colors.green,
color: Colors.red,
));
},
),
);
}
}
class CustomTimerPainter extends CustomPainter {
CustomTimerPainter({
this.animation,
this.backgroundColor,
this.color,
}) : super(repaint: animation);
final Animation<double> animation;
final Color backgroundColor, color;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = backgroundColor
..strokeWidth = 10.0
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
canvas.drawCircle(size.center(Offset.zero), size.width / 2.0, paint);
paint.color = color;
double progress = (1.0 - animation.value) * 2 * math.pi;
//print("progress ${progress}");
canvas.drawArc(Offset.zero & size, math.pi * 1.5, -progress, false, paint);
}
#override
bool shouldRepaint(CustomTimerPainter old) {
//print(animation.value);
return true;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MainPage(),
);
}
}

How to have a custom animation as a Flutter progress / loading indicator?

I am trying to replace the default CircularProgressIndicator with my own animation. I created a spinning widget based on the example here How to rotate an image using Flutter AnimationController and Transform? , but when replacing CircularProgressIndicator with "MyIconSpinner", for some reason it is not appearing. Any tips please?
Here are the contents of MyIconSpinner
import 'package:flutter/material.dart';
import 'package:flutter_icons/flutter_icons.dart';
class MyIconSpinner extends StatefulWidget {
MyIconSpinner({Key key, this.title}) : super(key: key);
final String title;
#override
_MyIconSpinnerState createState() => _MyIconSpinnerState();
}
class _MyIconSpinnerState extends State<MyIconSpinner>
with TickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 5000),
vsync: this,
);
super.initState();
}
#override
Widget build(BuildContext context) {
_controller.forward();
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RotationTransition(
turns: Tween(begin: 0.0, end: 1.0).animate(_controller),
child: Icon(
Icons.star,
size: 40,
),
),
],
),
),
);
}
}
I am placing it in a widget like this
return Scaffold(
appBar: AppBar(
title: Text("Appbar"),
backgroundColor: Colors.black,
automaticallyImplyLeading: false,
),
body: Center(
child: Column(children: <Widget>[
StreamBuilder(
stream: doSomething(withSomeData),
builder: (BuildContext context,
AsyncSnapshot<List<DocumentSnapshot>> asyncSnapshot) {
if (!asyncSnapshot.hasData) return MyIconSpinner();
I think you shouldn't wrap MyIconSpinner in Scaffold. You should give MyIconSpinner color parameter and also repeat animation after it is completed. Here is the edited version of MyIconSpinner.
class MyIconSpinner extends StatefulWidget {
MyIconSpinner({Key key, this.title, this.color = Colors.blue}) : super(key: key);
final String title;
final Color color;
#override
_MyIconSpinnerState createState() => _MyIconSpinnerState();
}
class _MyIconSpinnerState extends State<MyIconSpinner>
with TickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 5000),
vsync: this,
);
_controller.addListener((){
if(_controller.isCompleted){
_controller.repeat();
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
_controller.forward();
return RotationTransition(
turns: Tween(begin: 0.0, end: 1.0).animate(_controller),
child: Icon(
Icons.star,
size: 40,
color: widget.color,
),
);
}
}

How to SetState() of Parent when child is Updated in flutter

I have a list view and inside the list view, there is a child widget which can grow when user tap on that.
I want to scroll to the bottom of the list when the user taps on the child and it grows.
when I pass callback function from the parent to the child to scroll to the bottom.
and call the function when the user tap on the child.
I get the following error: setState() or markNeedsBuild() called during build.
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
ScrollController _controller = ScrollController();
void scrollToLast() {
print("trying to scroll");
setState(() {
_controller.animateTo(
_controller.position.maxScrollExtent,
duration: Duration(microseconds: 300),
curve: Curves.easeInOut,
);
});
}
#override
Widget build(BuildContext context) {
return ListView(
controller: _controller,
children: <Widget>[
MyChildWidget(
scrollToLast: this.scrollToLast,
)
],
);
}
}
class MyChildWidget extends StatefulWidget {
final VoidCallback scrollToLast;
MyChildWidget({
this.scrollToLast,
});
#override
_MyChildWidgetState createState() => _MyChildWidgetState(
scrollToLast: this.scrollToLast,
);
}
class _MyChildWidgetState extends State<MyChildWidget> {
final VoidCallback scrollToLast;
_MyChildWidgetState({
this.scrollToLast,
});
int count = 5;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
setState(() {
this.count += 5;
});
this.scrollToLast();
},
child: Column(
mainAxisSize: MainAxisSize.max,
children: List<Widget>.generate(
this.count,
(int index) => Container(
color: Colors.blue,
height: 30,
margin: EdgeInsets.all(10),
),
),
),
);
}
}
You can copy paste run full code below
You can use WidgetsBinding.instance.addPostFrameCallback
code snippet
void scrollToLast() {
print("trying to scroll");
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_controller.animateTo(
_controller.position.maxScrollExtent,
duration: Duration(microseconds: 300),
curve: Curves.easeInOut,
);
});
});
}
working demo
full code
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
ScrollController _controller = ScrollController();
void scrollToLast() {
print("trying to scroll");
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_controller.animateTo(
_controller.position.maxScrollExtent,
duration: Duration(microseconds: 300),
curve: Curves.easeInOut,
);
});
});
}
#override
Widget build(BuildContext context) {
return ListView(
controller: _controller,
children: <Widget>[
MyChildWidget(
scrollToLast: this.scrollToLast,
)
],
);
}
}
class MyChildWidget extends StatefulWidget {
final VoidCallback scrollToLast;
MyChildWidget({
this.scrollToLast,
});
#override
_MyChildWidgetState createState() => _MyChildWidgetState(
/*scrollToLast: this.scrollToLast,*/
);
}
class _MyChildWidgetState extends State<MyChildWidget> {
/* final VoidCallback scrollToLast;
_MyChildWidgetState({
this.scrollToLast,
});*/
int count = 5;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
setState(() {
this.count += 5;
});
widget.scrollToLast();
},
child: Column(
mainAxisSize: MainAxisSize.max,
children: List<Widget>.generate(
this.count,
(int index) => Container(
color: Colors.blue,
height: 30,
margin: EdgeInsets.all(10),
child: Text('$index'),
),
),
),
);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(
title: "Demo",
),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(child: MyWidget()),
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

SlideAnimation Not Starting Again After Calling SetState()

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().