Creating an Animated Progress Indicator in Flutter — Part 2
In the first part of this article series, we built a CircleProgressBar
that leveraged a CustomPaint
widget to draw out a circular progress indicator. In this part, we’ll update our CircleProgressBar
to handle animating value changes.
We’ll update what we had at the end of the last article:
and add smooth transitions between the different states so we can achieve the desired end result:
Converting to a Stateful Widget
The first thing we’ll need to do is convert our CircleProgressBar
stateless widget to a stateful widget. If you’re using Android Studio, you can easily convert a stateless widget to a stateful widget. Move your caret to the CircleProgressBar
class name and then use the Show Intention Actions action on the IDE. The stateful widget code now becomes:
Adding the Animation Controller
The next thing we want to do is add an animation controller to our widget’s state class. In the CircleProgressBarState
class, add the following field:
AnimationController _controller;
When constructing an animation controller, one must provide a TickerProvider
to the constructor’s vsync
parameter. An easy way to do this is to use the provided mixins SingleTickerProviderStateMixin
or TickerProviderStateMixin
. Since we only use one AnimationController
, we’ll go with the SingleTickerProviderStateMixin
which is more efficient. These mixins can only be used on a class of type State which is one reason why we add the animation controller to the state.
Update the CircleProgressBarState
class definition and add the mixin as shown below:
class CircleProgressBarState extends State<CircleProgressBar> with SingleTickerProviderStateMixin {
AnimationController _controller;
...
}
Next, create an instance of the animation controller which will be used to drive the animations. We need to create the animation controller as early as possible so we can have it available throughout the lifecycle of the widget. We do this by overriding the initState
function that’s called whenever the widget is inserted into the widget tree. We create an animation controller that takes 2 seconds to complete an animation and play it immediately. We’ll also dispose the animation controller when the state object is disposed.
@override
void initState() {
super.initState();
this._controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
this._controller.forward();
}@override
void dispose() {
this._controller.dispose();
super.dispose();
}
Remember to dispose your animation controller to avoid memory leaks.
Animating the Progress Bar
Since we have our animation controller, we’ll need to use it to drive the state change animation. We’ll start by wrapping our CustomPaint
widget in an AnimatedBuilder
widget.
An AnimatedBuilder
widget is useful for building animations. It accepts a Listenable
e.g. an animation and runs the builder every time a value change occurs.
For starters, we’ll pass in the animation controller and see what happens. Change your state’s build function to appear as shown below:
@override
Widget build(BuildContext context) {
final backgroundColor = this.widget.backgroundColor;
final foregroundColor = this.widget.foregroundColor;
return AspectRatio(
aspectRatio: 1,
child: AnimatedBuilder(
animation: this._controller,
child: Container(),
builder: (context, child) {
return CustomPaint(
child: child,
foregroundPainter: CircleProgressBarPainter(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
percentage: this._controller.value,
),
);
},
),
);
}
When you run this code, you’ll see that an animation plays and fills the progress bar. It’s not using our percent values to fill the progress bar.
Instead of using the controller’s value which always begins with 0 and ends with 1 interpolating all the values between over the duration specified, we’ll need to create a tween for the progress values.
Add a property to your state class of type Tween<double>
that will begin with 0 and end with our widget’s defined value.
Tween<double> valueTween;
Next, create an instance of your tween giving it the values to use for the animation.
this.valueTween = Tween<double>(
begin: 0,
end: this.widget.value,
);
Once that’s done, we’ll need to use our tween to update the painter’s progress value.
...
@override
Widget build(BuildContext context) {
...
foregroundPainter: CircleProgressBarPainter(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
percentage: this.valueTween.evaluate(this._controller),
),
...
}
...
Now whenever our widget starts, the progress value animates from 0 to the widget’s value over the controller’s duration. We could have created an animation out of our valueTween
and passed that over to the animation builder and our widget would still have worked the same way. The problem with that approach though is that it’s less flexible. A great thing about using tweens and evaluating them in the builder function is that we can have as many tweens as we want evaluated based on the controller or other animations. This will allow you to add different easing curves for each tween or one curve for all tweens. In fact, that’s what the complete example takes advantage of when animating the backgroundColor, foregroundColor and percentage to create the animation shown.
One thing we haven’t dealt with is animating when values change.
Animating State Changes
One problem we have is that when the widget value is changed, the progress bar remains at the old value. This is caused by the fact that we haven’t updated our tween with the new value passed to the widget. The best place to run this update is on the didUpdateWidget
function which is called before every call to the build function.
@override
void didUpdateWidget(CircleProgressBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (this.widget.value != oldWidget.value) {
// Try to start with the previous tween's end value. This ensures that we
// have a smooth transition from where the previous animation reached.
double beginValue =
this.valueTween?.evaluate(this._controller) ?? oldWidget?.value ?? 0;
// Update the value tween.
this.valueTween = Tween<double>(
begin: beginValue,
end: this.widget.value ?? 1,
);
this._controller
..value = 0
..forward();
}
}
If our widget value changes, we’re updating our value tween, resetting our controller and playing the updated animation.
One thing that’s important is calculating the begin
value of our new tween. Our animation’s begin value can be 1 of 3 possibilities. It could be:
- the current value of the tween based on the latest position of the animation controller. This helps us have a smooth transition if the widget is updated before the animation controller completes the animation.
- the value of the old widget.
- 0 if we couldn’t find a suitable value.
Once we have the begin value, all we have to do is update the valueTween
then reset the controller’s position back to 0 and set the controller to play the animation.
We finally have our animating progress bar.
One last thing we can do is add an easing curve and animate the color changes.
Getting Fancy
For the finishing touches, we’ll try to add an easing curve to our animation and animating color changes. We’ll make use of the CurvedAnimation
class that defines its parent animation’s progress as a non-linear curve.
class CircleProgressBarState extends State<CircleProgressBar>
with SingleTickerProviderStateMixin {
...
Animation<double> curve; ...
@override
void initState() {
...
this.curve = CurvedAnimation(
parent: this._controller,
curve: Curves.easeInOut,
);
...
}
...
}
Once we have our curve, we need to use it in places where we’d initially used the controller. We can pass it into our AnimatedBuilder
and use it in our evaluate
function calls.
To animate color changes, we need to use tweens of type Tween<Color>
and evaluate them in the AnimatedBuilder
’s builder
function.
class CircleProgressBarState extends State<CircleProgressBar>
with SingleTickerProviderStateMixin {
...
Tween<Color> foregroundColorTween ...
@override
void didUpdateWidget(CircleProgressBar oldWidget) {
super.didUpdateWidget(oldWidget);
...
this.foregroundColorTween = ColorTween(
begin: oldWidget?.foregroundColor,
end: this.widget.foregroundColor,
);
...
} @override
Widget build(BuildContext context) {
... child: AnimatedBuilder(
animation: this.curve,
...
builder: (context, child) {
final foregroundColor =
this.foregroundColorTween?.evaluate(this.curve) ??
this.widget.foregroundColor;
return CustomPaint(
child: child,
foregroundPainter: CircleProgressBarPainter(
backgroundColor: this.widget.backgroundColor,
foregroundColor: foregroundColor,
percentage: this.valueTween.evaluate(this.curve),
),
);
},
),
}
}
If you’re looking for an opportunity to use what you’ve learnt, you can try animating the backgroundColor changes using the same technique as the foregroundColor above before you look at the source code.
The complete source code for this part can be found on the GitHub repository.