Flutter determinate Linear Progress Bar not animating after the value changes - flutter

I want my linear progress bar to update progressively in stages, each time a user answers a question correctly in a quiz I have made.
I can't seem to get the progress bar to animate upon the value changes, however.
To note:
-My animation runs successfully, if I call my _animationController.forward from initState() (with some test values in the tween).
-I notify my AnimationController to run via notifications triggered using the Provider package as shown in the code below, when a question has been correctly answered, which is working.
-there are two values (original and new) that are input into the animation tween to tell the progress bar how much it should animate each time, are also correctly updating as this log shows.
I/flutter (18664): original value 0.0 and new value 0.0
I/flutter (18664): original value 0.0 and new value 0.25
import 'package:flutter/material.dart';
import 'package:learn_to_read/controllers/answerManager.dart';
import 'package:provider/provider.dart';
class AnswerProgressBar extends StatefulWidget {
const AnswerProgressBar({Key? key}) : super(key: key);
#override
_AnswerProgressBarState createState() => _AnswerProgressBarState();
}
class _AnswerProgressBarState extends State<AnswerProgressBar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _anim;
late double _originalValue = 0;
late double _newValue = 0;
late int _localPoints;
late double _valueCompleted;
#override
void initState() {
double _originalValue = 0.0;
double _newValue = 0.0;
_animationController = AnimationController(
duration: Duration(milliseconds: 800), vsync: this);
_anim = Tween<double>(begin: _originalValue, end: _newValue)
.animate(_animationController)..addListener(() {
setState(() {
});
});
_localPoints = 0;
_valueCompleted = 0;
super.initState();
}
#override
void didChangeDependencies() {
_originalValue = _newValue;
if (Provider.of<AnswerManager>(context).pageTilesCompleted &&
_valueCompleted != 0) {
_localPoints++;
} else {
_localPoints = Provider.of<AnswerManager>(context).points;
}
_valueCompleted =
_localPoints / Provider.of<AnswerManager>(context).totalTilesPerPage;
_newValue = _valueCompleted;
_animationController.forward();
super.didChangeDependencies();
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
print('original value $_originalValue and new value $_newValue');
return AnimatedBuilder(
animation: _animationController,
builder: (context, i) => LinearProgressIndicator(
value: _anim.value,
backgroundColor: Colors.grey
),
);
}
}

Use set state
This will change the state as user will answer

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

Flutter: re-render child widget on parent state change

So a have a stateful parent with a timer value. This timer can be reset by the user. I also have a child stateful widget with an animation controller. The parent passes the timer value to this child to reset the animation. Here is some code:
class _ParentState extends State<Parent> {
int timer = 20;
void _userInput() {
setState(() {
timer = 20;
}
}
#override
Widget build(BuildContext context) {
return Child(
time: timer
);
}
}
Note: this is striped down for simplicity
So when my user triggers the _userInput method, the timer value gets changed but the child doesn't. Here is my full child widget:
class TimerProgress extends StatefulWidget {
TimerProgress({
Key key,
#required this.time,
#required this.onCompleted
}) : super(key: key);
final int time;
final Function onCompleted;
#override
_TimerProgressState createState() => _TimerProgressState();
}
class _TimerProgressState extends State<TimerProgress> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _widthAnimation;
#override
void initState() {
_controller = AnimationController(
duration: Duration(seconds: widget.time),
vsync: this
);
_widthAnimation = Tween<double>(
begin: 200,
end: 0
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.linear
));
_controller.addListener(() {
if (_controller.value == 1) {
widget.onCompleted();
}
});
_controller.forward();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ...
}
}
So this all works, but ideally I want the animation to be reset, which doesn't work...
initState runs only once per state instance.
Implement resetting the time inside didUpdateWidget: https://api.flutter.dev/flutter/widgets/RawScrollbarState/didUpdateWidget.html
You can't (cause it will cause recursive loop, setState will trigger didUpdateWidget again) and shouldn't (after this method is invoked, build is being called) have to call setState inside this method.
Runnable example: https://www.dartpad.dev/848976603f866f4e3aa6030aba8addf7?null_safety=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...).

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

How to animate old page during transition in flutter?

I made a custom transition for my iOS project, and now I want to move the project to Flutter. The transition is fading out the old page, and fading in the new one.
But I cannot achieve this by overriding the PageRoute.
I did some research on this:
There's a similar question
Animate route that is going out / being replaced
From the accepted answer, I know there's a parameter 'secondaryAnimation' which may be useful to achieve it, but after trying to use the code from it, I still cannot animate the old page, all transitions have happened to the new page (the 'child' widget).
Can I get an 'old page' instance from the buildTransition method for animating? Or is there a better way to animate the old page?
Thanks!
I think that secondaryAnimation is used when transitioning to another page. So for your initial route, you'd have to specify the animation of it going away using secondaryAnimation and on your second page you use animation to animate how it appears.
It's a bit awkward that you have to use secondaryAnimation when creating the first route, because it means that it will be used for any transition away from that route. So, with PageRouteBuilder, you can't, for example, let the old page slide to the left when transitioning to page B but slide up when transitioning to page C.
I wrote two classes to achieve animating the old page.
PageSwitcherBuilder is a widget builder to animate old page.
PageSwitcherRoute is a route class to navigate new page.
Examples on DartPad
Here is my script.
class PageSwitcherBuilder extends StatefulWidget {
const PageSwitcherBuilder(
{Key? key,
required this.builder,
this.duration = const Duration(milliseconds: 500),
this.reverseDuration = const Duration(milliseconds: 500)})
: super(key: key);
final Duration duration;
final Duration reverseDuration;
final Widget Function(AnimationController controller) builder;
#override
State<PageSwitcherBuilder> createState() => _PageSwitcherBuilderState();
}
class _PageSwitcherBuilderState extends State<PageSwitcherBuilder>
with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: widget.duration,
reverseDuration: widget.reverseDuration,
);
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return widget.builder(_controller);
}
}
class PageSwitcherRoute extends PageRouteBuilder {
PageSwitcherRoute({
required this.controller,
required Widget page,
}) : super(pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return page;
}) {
willDisposeAnimationController = false;
}
#override
final AnimationController controller;
#override
AnimationController createAnimationController() {
return controller;
}
}
You may also check this ZoomPageTransitionsBuilder class to change default transitions.
Workaround that I've come up with is to observe pushes and pops of routes and animte during these.
Create a Route Wrapper using RouteObserver
Run animation on didPushNext
Reverse animation on didPopNext
Wrap all screens that you want to animate on exit and on reenter.
import 'package:flutter/material.dart';
class ScreenSlideTransition extends StatefulWidget {
const ScreenSlideTransition({super.key, required this.child});
final Widget child;
#override
State<ScreenSlideTransition> createState() => _ScreenSlideTransitionState();
}
class _ScreenSlideTransitionState extends State<ScreenSlideTransition>
with RouteAware, TickerProviderStateMixin {
late final AnimationController _controller;
#override
Widget build(BuildContext context) {
final screenWidth = -MediaQuery.of(context).size.width;
return AnimatedBuilder( // Whatever animation you need goes here
animation: _controller,
builder: (BuildContext context, Widget? child) => Transform.translate(
offset: Offset(screenWidth * _controller.value, 0),
child: widget.child));
}
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}
#override
void dispose() {
_controller.dispose();
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPopNext() {
_controller.reverse();
super.didPopNext();
}
#override
void didPushNext() {
_controller.forward();
super.didPushNext();
}
}
In order to make the above code work make sure to initialise RouteObserver (see docs)
final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();
void main() {
runApp(MaterialApp(
home: Container(),
navigatorObservers: <RouteObserver<ModalRoute<void>>>[ routeObserver ],
));
}
Remember to set the same Duration for AnimationController in ScreenSlideTransition and duration for transitionsBuilder if you want to synchronise in/out animation.