How to dispose controller on demand? - flutter

Edit-: A relevant question also would be How to dispose multiple controllers dynamically?
I have a custom painter in which each object needs to be animated individually. And after certain time data gets added to the list and I paint it and animate it.
Here what it looks like,
The is the model class for animation
class Wave {
AnimationController animationController;
Animation<double> animation;
bool isDisposed;
Wave({
required this.animationController,
required this.animation,
this.isDisposed = false,
});
}
#override
void paint(Canvas canvas, Size size) {
for (var i = 0; i < waveData.length; i++) {
addAnimatedWaves(i); // <------- a callback to add animation model to list
animatedWaves[i].animationController.forward();
canvas.drawLine(...);
if (animatedWaves[i].animationController.status ==
AnimationStatus.dismissed &&
!animatedWaves[i].isDisposed) { //<------ trying to dispose but it won't get executed
animatedWaves[i].animationController.dispose();
animatedWaves[i].animation.removeListener(() {});
animatedWaves[i].isDisposed = true;
}
}
}
This is the above mentioned call back,
void addAnimatedWaves(int i) {
if (_waves.isEmpty) {
var controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));
_waves.add(Wave(
animationController: controller,
animation: Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: controller, curve: Curves.easeIn),
),
));
} else {
if (_waves.length > i) {
var controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));
_waves.add(Wave(
animationController: controller,
animation: Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: controller, curve: Curves.easeIn),
),
));
}
_waves[i].animation.addListener(() {
if (mounted) setState(() {});
});
}
}
disposing
#override
void dispose() {
for (var wave in _waves) { // it always calls every dispose method because every
// controller is active
if (!wave.isDisposed) {
wave.animationController.dispose();
wave.animation.removeListener(() {});
}
}
super.dispose();
}
what I want is to immediately dispose a controller when the animation is completed. And
optimise the for loop at the end so that it doesn't have length of whole list.
So can anyone suggest what should do or Is there any better method to animate list of objects which needs to individually animated?

Ok so I have found this solution if anyone have better solution then I'm happy to change the accepted answer.
First, #pskink suggested that we should not use controllers inside paints so removed them and added inside addAnimatedWave()
#override
void paint(Canvas canvas, Size size) {
if (animatedWaves.isEmpty) {
addAnimatedWaves(i);//---->for first time
} else {
if (animatedWaves.isNotEmpty && i >= animatedWaves.length - 1) {
addAnimatedWaves(i);
}
}
canvas.drawLine(...);
}
the above condition checks whether the index contains any value or not.
So if it contains then it is not required and it skips it.
void _addAnimatedWave(int i) {
if (_waves.isEmpty) {
_addWave(); // <--- it just creates new controller and adds to _waves
//you can refer it from question.
} else {
if (_waves.length > i) {
_addWave();
}
_waves[i].animationController.forward();
_waves[i].animation.addListener(() {
if (mounted) setState(() {});
});
_waves[i].animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_waves[i].animationController.dispose();
_waves[i].animation.removeListener(() {});
_waves[i].isDisposed = true;
}
});
}
}
Here we are adding a listener to every AnimationController but it will active till an animation completes for the controller and we are immediately disposing it.
so for my case a single controller will be active for 300ms.
Before it was adding 1000+ controller every 2-3 seconds now it only adds a controller for every wave bar.
Now for the remaining controller which are still active, while widget is removed from widget tree.
#override
void dispose() {
for (var i = _waves.length - 1; i >= 0; i--) {
if (_waves[i].isDisposed) {
break;
}
_waves[i].animationController.dispose();
_waves[i].animation.removeListener(() {});
}
super.dispose();
}
For my case disposing a controller was linear so if we reverse the list and reach to the first controller which was disposed, after that every controller would be disposed so we don't require to go through whole list so we can just break that loop.
And with this optimisation, I notice only max 3 controllers were required to be disposed with for-loop.

Related

Flutter SizeTransition only at first widget build

I made a widget 'ExpandableSection' which animates the child content by using a SizeTransition. It also works fine so far but I would like to have an additional parameter like "disableAtFirstBuild" so there is no initial animation but the widget is instantly shown. And only on rebuild the animation should be triggered. It seems like an easy task but I searched multiple hours for a solution without luck. For example I tried to set the animation duration to zero at first, invert a state boolean to save the fact that one build is done and afterwards set the duration to the normal value again. But somehow you cannot change an active controller. Is there a way to do it? Maybe it is quite easy and obvious but it is just not clear to me.
Any help would be much appreciated.
import 'package:flutter/widgets.dart';
class ExpandableSection extends StatefulWidget {
final Widget? child;
final bool? expand, useSliverScrollSafeMode;
final Axis axis;
ExpandableSection({this.expand = false, this.useSliverScrollSafeMode = false, this.axis = Axis.vertical, this.child});
#override
_ExpandableSectionState createState() => _ExpandableSectionState();
}
class _ExpandableSectionState extends State<ExpandableSection> with SingleTickerProviderStateMixin {
late AnimationController expandController;
late Animation<double> animation;
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
void prepareAnimations() {
expandController = AnimationController(vsync: this, duration: const Duration(milliseconds: 650));
animation = CurvedAnimation(
parent: expandController,
curve: Curves.easeInOutQuart,
);
}
void _runExpandCheck() {
if (widget.expand!) {
expandController.forward();
} else {
expandController.reverse();
}
}
#override
void didUpdateWidget(ExpandableSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
#override
void dispose() {
expandController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.useSliverScrollSafeMode! && !expandController.isAnimating && !widget.expand!) {
return SizedBox.shrink();
} else {
return SizeTransition(axisAlignment: -1, axis: widget.axis, sizeFactor: animation, child: Center(child: widget.child));
}
}
}
Add 'value' to your controller while creating it; like this:
expandController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 650),
value: widget.expand ? 650 : 0,
);
Try stoping the animation controller in initState and then calling .forward() whenever you want it to run.
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
expandController.stop();
}

Unable to reliably trigger animation with longPress on GestureDetector

I'm trying to create a button with a progress indicator (CircularProgressIndicator)
Desired flow:
The user taps on the button it should fire a function
The user presses the button (and holds), it should trigger the animation and fire a function
When the user releases their hold, it should reset the animation and fire a function
At this point, my code works on the second time pressing (and holding) the element. The first time around, the animation controller's addListener prints 2-3 times and then stops, whereas the second time, it holds true and continues to print as the user holds the element. Ontap functionality works regardless.
It's happening while running locally on an android and an ios device
Stripped code block:
import 'package:flutter/material.dart';
import 'package:homi_frontend/constants/woopen_colors.dart';
class ProgressButton extends StatefulWidget {
ProgressButton({
#required this.onTap,
#required this.onLongPress,
#required this.onLongPressUp,
this.duration = const Duration(seconds: 60),
});
final Function onTap;
final Function onLongPress;
final Function onLongPressUp;
final Duration duration;
#override
ProgressButtonState createState() => ProgressButtonState();
}
class ProgressButtonState extends State<ProgressButton>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
bool _beingPressed = false;
#override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: widget.duration,
);
_animationController.addListener(_animationListener);
_animationController.addStatusListener(_animationStatusListener);
super.initState();
}
void _animationListener() {
print('Animation Controller Listener');
setState(() {});
}
void _animationStatusListener(AnimationStatus status) {
print('_animationStatusListener');
if (status == AnimationStatus.completed) {
print(
'Completed duration of ${widget.duration}, fire _handleOnLongPressUp');
_handleOnLongPressUp();
}
if (status == AnimationStatus.forward) {
this.setState(() {
_beingPressed = true;
});
}
}
void _handleOnLongPress() {
print('_handleOnLongPress');
try {
_animationController.forward();
} catch (e) {
print('_handleOnLongPress error: ${e.toString()}');
} finally {
if (_animationController.status == AnimationStatus.forward) {
print('Controller has been started, fire widget.onLongPress');
widget.onLongPress();
}
}
}
void _handleOnLongPressUp() {
print('_handleOnLongPressUp');
try {
this.setState(() {
_beingPressed = false;
});
_animationController.reset();
} catch (e) {
print('_handleOnLongPressUp error: ${e.toString()}');
} finally {
if (_animationController.status == AnimationStatus.dismissed) {
print('Controller has been dismissed, fire widget.onLongPressUp');
widget.onLongPressUp();
}
}
}
#override
dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
key: Key('progressButtonGestureDetector'),
behavior: HitTestBehavior.opaque,
onLongPress: _handleOnLongPress,
onLongPressUp: _handleOnLongPressUp,
onTap: widget.onTap,
child: Container(
width: 80,
height: 80,
child: Text(_animationController.value.toStringAsFixed(2)),
),
);
}
}
Output:
flutter: _handleOnLongPress
flutter: _animationStatusListener
flutter: Controller has been started, fire widget.onLongPress
(2) flutter: Animation Controller Listener
# here it just seems to loose its connection, but if I press (and hold) again, I get:
flutter: _handleOnLongPress
flutter: _animationStatusListener
flutter: Controller has been started, fire widget.onLongPress
(326) flutter: Animation Controller Listener
flutter: _handleOnLongPressUp
flutter: Animation Controller Listener
flutter: _animationStatusListener
flutter: Controller has been dismissed, fire widget.onLongPressUp
I've also looked briefly into RawGestureDetector but only my TapGestureRecognizer gestures seem to fire, the LongPressGestureRecognizer ones don't... even if TapGestureRecognizers are removed.
_customGestures = Map<Type, GestureRecognizerFactory>();
_customGestures[TapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = (TapDownDetails details) {
print('onTapDown');
}
..onTapUp = (TapUpDetails details) {
print('onTapUp');
}
..onTap = () {
print('onTap');
}
..onTapCancel = () {
print('onTapCancel');
};
},
);
_customGestures[LongPressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
duration: widget.duration, debugOwner: this),
(LongPressGestureRecognizer instance) {
instance
..onLongPress = () {
print('onLongPress');
}
..onLongPressStart = (LongPressStartDetails details) {
print('onLongPressStart');
_animationController.forward();
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
print('onLongPressMoveUpdate');
}
..onLongPressEnd = (LongPressEndDetails details) {
print('onLongPressEnd');
_animationController.reset();
}
..onLongPressUp = () {
print('onLongPressUp');
};
},
);
Please & thank you for your time!
You are using a Text widget to receive the hit within the
GestureDetector, which have a small hit box compare to the thumb. This might be the reason why you might misclick the hit box occasionally.
You can use the debugPaintPointersEnabled to see the behavior more clearly (need to do a Hot Restart if the app is running):
import 'package:flutter/rendering.dart';
void main() {
// Add the config here
debugPaintPointersEnabled = true;
runApp(App());
}
You can see that the hit box does not flash all the time, even when we think we hit the Text. To increase the accuracy, let's wrap a Container with size around the Text
GestureDetector(
// ... other lines
child: Container(
width: 100,
height: 50,
color: Colors.blue,
alignment: Alignment.center,
child:
Text('Value: ${_animationController.value.toStringAsFixed(2)}')),
);
You can see that the hit box flashes everytime now

Showing Simple Duration timer left on screen

Is there any simple way that i can show the time remaing in DURATION on screen?
void onTapDown(TapDownDetails details){
if(state == State.menu){
timer = Timer(duration = new Duration(seconds: 5), () {
state = State.playing;
});
print("STARTING IN [secondsremaing]");
}
or should i have to make it complex and implement any other class to do so ?
It will not work that way because a Timer will only call the given callback after the given Duration, but it will not tick for you.
If you want to show an updated widget indicating the remaining time, you will have to use a ticker and this is normally achieved by setting up an AnimationController.
In your case, it could look something like this (assuming you are in a StatefulWidget):
class _YourWidgetsState extends State<YourWidget> with SingleTickerProviderMixin {
AnimationController remainingTimeController;
#override
void initState() {
super.initState();
remainingTimeController = AnimationController(vsync: this, duration: const Duration(seconds: 5));
}
/// Used somewhere in your build method.
void onTapDown(TapDownDetails details) {
if(state == State.menu){
timer = Timer(duration = new Duration(seconds: 5), () {
state = State.playing;
});
print("STARTING IN [secondsremaing]");
}
}
#override
Widget build(BuildContext context) {
// Obviously return your other widgets in here as well.
return AnimatedBuilder(
animation: remainingTimeController,
builder: (context, _) => Text('${remainingTimeController.value * 60 * 5}'),
);
}
}

Repeating animations specific times e.g 20 times by Flutter

I have a simple animation :
Animation<double> animation;
Tween<double> tween = Tween(begin: 1, end: 1.15);
AnimationController animationController;
#override
void initState() {
super.initState();
animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
animation = animationController.drive(tween);
}
What i want ?
make this animation repeats e.g 20 times .
What i tried before posted this question ?
1- This method not have a parameter or way that makes me repeats this animation e.g 20 times.
it repeats and repeats and repeats forever :
animationController.repeat();
2- Use simple loops . It hangs my app, it requires to stop entire application and rerun it to solve that hanging . It's seems that loops completely not dealing with futures :
do {
animationController.forward().then((x) {
animationController.reverse().then((x) {
repeats++;
});
});
} while (repeats < 20);
3- Making int variable and add a listener ..etc,it's working , but seems that it's not the best way to do that matter :
int repeats = 0;
animation.addStatusListener((status) {
if (repeats < 20) {
if (status == AnimationStatus.completed) {
animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
animationController.forward();
}
repeats++;
} });
4- make chain of then . **No comment on it ** 😂😂😂 :
animationController.forward().then((x) {
animationController.reverse().then((x) {
animationController.forward().then((x) {
animationController.reverse().then((x) {
animationController.forward().then((x) {
animationController.reverse();
});
});
});
});
});
Now , shortly how can i repeat animation 20 times
You can try it:
5 - Use TickerFuture return from repeat, set timeout then stop animation.
AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 3),
);
_animationController.addListener(() => setState(() {}));
TickerFuture tickerFuture = _animationController.repeat();
tickerFuture.timeout(Duration(seconds: 3 * 10), onTimeout: () {
_animationController.forward(from: 0);
_animationController.stop(canceled: true);
});
}
This can be easily done without the timer with extension method:
extension on AnimationController {
void repeatEx({#required int times}) {
var count = 0;
addStatusListener((status) {
if (status == AnimationStatus.completed) {
if (++count < times) {
reverse();
}
} else if (status == AnimationStatus.dismissed) {
forward();
}
});
}
}
And you could use it like this:
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)
..repeatEx(times: 10)
..forward();
If you have a Widget with a playing argument that can set to true or false dynamically (to start and stop animation dynamically), you can improve behaviour by using a cancellable Timer() instead of Future.timeout :
void updatePlaying() {
if (widget.playing != false && !_controller.isAnimating && !_loopHasCompleted) {
_controller.repeat();
_timeout?.cancel();
_timeout = Timer(widget.duration * widget.loops, () {
_controller.reset();
_loopHasCompleted = true;
});
} else if (widget.playing == false) {
if (_controller.isAnimating)
_controller.reset();
_loopHasCompleted = false;
}
}
AnimationController(
lowerBound: 0.0,
upperBound: 20.0,
vsync: this, duration: Duration(milliseconds: 300))
It's easy to use just like below
var counter = 0;
int repeatTimes = 10 // how many times you want to repeat animation
animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
if (++counter < repeatTimes) {
animation.reverse();
}
} else if (status == AnimationStatus.dismissed) {
animation.forward();
}
});
I created a simple widget for this purpose
import 'package:flutter/widgets.dart';
import 'package:lottie/lottie.dart';
class LottieRepeat extends StatefulWidget {
final String asset;
final int repeatTimes;
final VoidCallback onComplete;
const LottieRepeat(
{Key? key,
required this.asset,
required this.repeatTimes,
required this.onComplete})
: super(key: key);
#override
State<LottieRepeat> createState() => _MyLottieRepeatState();
}
class _MyLottieRepeatState extends State<LottieRepeat>
with TickerProviderStateMixin {
late final AnimationController _controller;
int count = 0;
#override
void initState() {
super.initState();
/// validate
if (widget.repeatTimes <= 0) throw Exception('invalid repeat time');
_controller = AnimationController(vsync: this);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
count++;
if (count < widget.repeatTimes) {
_controller.reset();
_controller.forward();
} else {
widget.onComplete();
}
}
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Lottie.asset(
widget.asset,
controller: _controller,
onLoaded: (composition) {
// Configure the AnimationController with the duration of the
// Lottie file and start the animation.
_controller
..duration = composition.duration
..forward();
},
);
}
}
sample usage:
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: LottieRepeat(
asset: 'assets/image/tick.json',
repeatTimes: 2,
onComplete: () => Navigator.of(context).pop(true),
),
);
});

Listening to a variable change in flutter

I'm trying to listen to a variable change to execute some code. So the variable is a bool named reset. I want to execute something (say reset the animation controller) once the animation ends OR a button (from another widget) is pressed. Executing something when the animation ends works as once it ends AnimationStatus.dismissed will be its state and the listener will be called. This then allows me to use a callback function onCountdownexpire in order to set the variable reset accordingly and based on what it is set, execute some code in the if(widget.reset) block. So there is no need to listen for this case.
Problem:
However, lets say a button is pressed (in another widget) and I set the variable reset to true. I want the animation to stop and reset. I cannot now depend on the AnimationStatus listener as it only executes when there is a state change. I want it to reset during its state. So I have to somehow listen to the variable widget.reset.
I have done some research on this and found out that ValueNotifier might be a way to go but there are not a lot of examples on how to go about using it. Like how do I go about listening to it ?
Code:
class Countdown extends StatefulWidget {
final VoidCallback onCountdownExpire;
bool reset;
Countdown(this.onCountdownExpire);
#override
CountdownState createState() => CountdownState();
}
class CountdownState extends State<Countdown> with TickerProviderStateMixin {
AnimationController controller;
String get timerString {
Duration duration = controller.duration * controller.value;
return '${duration.inMinutes}:${(duration.inSeconds % 60).toString()}';
}
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..addStatusListener((AnimationStatus status){
if (status == AnimationStatus.dismissed) {
debugPrint("Animation.dismissed");
widget.onCountdownExpire();
if (widget.reset) {
widget.reset = false;
controller.reverse(from: 1.0);
}
}
});
controller.reverse(from: 1.0);
}
... // omitted code
}
What I have tried but it does not seem to be working as expected:
class Countdown extends StatefulWidget {
final VoidCallback onCountdownExpire;
Countdown(this.onCountdownExpire);
ValueNotifier reset = ValueNotifier(false);
#override
CountdownState createState() => CountdownState();
}
class CountdownState extends State<Countdown> with TickerProviderStateMixin {
AnimationController controller;
#override
void initState() {
widget.reset.addListener(() {
debugPrint("value notifier is true");
controller.reverse(from: 1.0);
});
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..addStatusListener((AnimationStatus status){
if (status == AnimationStatus.dismissed) {
debugPrint("Animation.dismissed");
widget.onCountdownExpire();
}
});
controller.reverse(from: 1.0);
}
... // omitted code
}
Update: The solution (above) was actually working, I just had to use something like this to notify the listener:
countdown.reset.notifyListeners();
// OR
countdown.reset.value = true;
OP already got an answer in the comments. I'm just typing it here so the question is correctly marked as answered.
Just notify the listener:
countdown.reset.notifyListeners();
// OR
countdown.reset.value = true;
Credit to #pskink
Use of overwriting getter/setters
You can make use of the getter and setter functions.
In the example below we are printing happy birthday when the person's age changes:
class Person {
int _age;
Person(this._age);
int get age => _age;
set age(int newValue) {
print("Happy birthday! You have a new Age.");
//do some awsome things here when the age changes
_age = newValue;
}
}
The usage is exactly as you are used to:
final newPerson = Person(20);
print(newPerson.age); //20
newPerson.age = 21; //prints happy birthday
print(newPerson.age); //21