Showing Simple Duration timer left on screen - flutter

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}'),
);
}
}

Related

Flutter How can I use setState and only change one area and leave the other as it is?

I want a page that has a timer and also displays math problems. and whenever the correct answer has been entered, a new task should appear. But the problem is that whenever a new task appears, the timer is reset. How can I prevent this ?
late Timer timer;
double value = 45;
void startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (value > 0) {
setState(() {
value--;
});
} else {
setState(() {
timer.cancel();
});
}
});
}
#override
void initState() {
// TODO: implement initState
super.initState();
startTimer();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
void userTextFieldInput() {
controller.addListener(
() {
String rightResult = (firstIntValue + secondIntValue).toString();
String userResult = controller.text;
if (rightResult == userResult) {
setState(() {
DatabaseHelper(
firstUserValue: firstIntValue,
secondUserValue: secondIntValue,
finalUserResult: int.parse(controller.text),
).setDB();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const CalculatePage(),
),
);
});
} else if (controller.text.length >= 2) {
controller.clear();
}
},
);
}
You should create two different state, one for timer and another one for the problems.
You can use the package flutter bloc to manage these state easily.

How to dispose controller on demand?

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.

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();
}

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),
),
);
});

Show timer progress on a CircularProgressIndicator in flutter

I'm using a RestartableTimer (subclass of Timer) as a countdown timer, to "kick people out" of a form after a certain duration.
I would like to display the progress of that timer, and I like the idea of a circular progress slowly filling up.
I'm not showing my code because I don't really have anything to show. I have a completely static progress indicator and a working timer, in a widget (stateful or stateless, whichever works best).
I face two issues and this is where I need help for :
I don't know how to check every x milliseconds for the timer progress. How can I do that? I don't need copy-pasta code, but more of "what object / which direction" should I go for?
The timer progress in ticks is not implemented (NotImplementedException) ; is there any way to have an equivalent somewhere else? That object works really well for me, except for that part.
Am I SOL or is there a way to make it?
There's nothing to be implemented in the getter tick, since RestartableTimer is not periodic. What you want is a much more complex thing, and RestartableTimer is not able to help you with that.
First, you need something to control the progress of the CircularProgressIndicator:
class ProgressController {
static const double smoothnessConstant = 250;
final Duration duration;
final Duration tickPeriod;
Timer _timer;
Timer _periodicTimer;
Stream<void> get progressStream => _progressController.stream;
StreamController<void> _progressController = StreamController<void>.broadcast();
Stream<void> get timeoutStream => _timeoutController.stream;
StreamController<void> _timeoutController = StreamController<void>.broadcast();
double get progress => _progress;
double _progress = 0;
ProgressController({#required this.duration})
: assert(duration != null),
tickPeriod = _calculateTickPeriod(duration);
void start() {
_timer = Timer(duration, () {
_cancelTimers();
_setProgressAndNotify(1);
_timeoutController.add(null);
});
_periodicTimer = Timer.periodic(
tickPeriod,
(Timer timer) {
double progress = _calculateProgress(timer);
_setProgressAndNotify(progress);
},
);
}
void restart() {
_cancelTimers();
start();
}
Future<void> dispose() async {
await _cancelStreams();
_cancelTimers();
}
double _calculateProgress(Timer timer) {
double progress = timer.tick / smoothnessConstant;
if (progress > 1) return 1;
if (progress < 0) return 0;
return progress;
}
void _setProgressAndNotify(double value) {
_progress = value;
_progressController.add(null);
}
Future<void> _cancelStreams() async {
if (!_progressController.isClosed) await _progressController.close();
if (!_timeoutController.isClosed) await _timeoutController.close();
}
void _cancelTimers() {
if (_timer?.isActive == true) _timer.cancel();
if (_periodicTimer?.isActive == true) _periodicTimer.cancel();
}
static Duration _calculateTickPeriod(Duration duration) {
double tickPeriodMs = duration.inMilliseconds / smoothnessConstant;
return Duration(milliseconds: tickPeriodMs.toInt());
}
}
Then you can implement a CircularProgressIndicator that listens to the Streams from ProgressController:
class RestartableCircularProgressIndicator extends StatefulWidget {
final ProgressController controller;
final VoidCallback onTimeout;
RestartableCircularProgressIndicator({
Key key,
#required this.controller,
this.onTimeout,
}) : assert(controller != null),
super(key: key);
#override
_RestartableCircularProgressIndicatorState createState() =>
_RestartableCircularProgressIndicatorState();
}
class _RestartableCircularProgressIndicatorState
extends State<RestartableCircularProgressIndicator> {
ProgressController get controller => widget.controller;
VoidCallback get onTimeout => widget.onTimeout;
#override
void initState() {
super.initState();
controller.progressStream.listen((_) => updateState());
controller.timeoutStream.listen((_) => onTimeout());
}
#override
Widget build(BuildContext context) {
return CircularProgressIndicator(
value: controller.progress,
);
}
void updateState() => setState(() {});
}
You can also pass some of the paramers of CircularProgressIndicator to RestartableCircularProgressIndicator, so you can customize it.
A usage example:
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ProgressController controller;
#override
void initState() {
super.initState();
controller = ProgressController(
duration: Duration(seconds: 5),
);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RestartableCircularProgressIndicator(
controller: controller,
onTimeout: () => print('timeout'),
),
RaisedButton(
onPressed: controller.start,
child: Text('Start'),
),
RaisedButton(
onPressed: controller.restart,
child: Text('Restart'),
),
],
),
),
),
);
}
}
I'll convert this into a library someday, but until then I cannot provide the tests and documentation to this code, so you have to study it if you want to understand what's going on here (I'm sorry...).