Animated Progress Bar Widget in Flutter
Introduction
Progress bars are an essential UI element for indicating the status of a task, such as file uploads, downloads, or data processing. While a static progress bar conveys information, an animated one significantly enhances user experience by providing a smoother, more engaging visual feedback. In Flutter, creating such an animated widget is straightforward using its powerful animation framework.
This article will guide you through building a reusable animated progress bar widget in Flutter, covering the core animation concepts and practical implementation steps.
Core Animation Concepts
Before diving into the code, let's briefly review the key Flutter animation components we'll be using:
AnimationController: This class manages an animation. It can start, stop, forward, or reverse an animation, and also set its duration. It requires aTickerProvider(typically provided bySingleTickerProviderStateMixinorTickerProviderStateMixin) to synchronize the animation with the screen refresh rate.Tween: ATween(short for "between") defines a range of values over which an animation should occur. For instance, aTweenwill animate a double value from 0.0 to 1.0.(begin: 0.0, end: 1.0) Animation: AnAnimationobject holds the current value of an animation. ATweencan generate anAnimationby calling itsanimate()method with anAnimationController.addListener()andsetState(): To update the UI as the animation progresses, you attach a listener to theAnimation. Inside the listener,setState()is called to rebuild the widget tree with the new animation value.didUpdateWidget(): This lifecycle method is crucial for handling changes to the widget's properties (like theprogressvalue) and ensuring the animation correctly updates from its current state to the new target state.SingleTickerProviderStateMixin: A mixin that provides a singleTickerfor anAnimationController. It's typically used withStatefulWidget.
Step-by-Step Implementation
1. Create the Widget Structure
We'll start by defining a StatefulWidget named AnimatedProgressBar. This widget will accept parameters for its current progress, colors, height, border radius, and animation duration, making it highly customizable.
import 'package:flutter/material.dart';
class AnimatedProgressBar extends StatefulWidget {
final double progress; // Value from 0.0 to 1.0
final Color backgroundColor;
final Color progressBarColor;
final double height;
final BorderRadiusGeometry borderRadius;
final Duration animationDuration;
const AnimatedProgressBar({
Key? key,
required this.progress,
this.backgroundColor = Colors.grey,
this.progressBarColor = Colors.blue,
this.height = 10.0,
this.borderRadius = const BorderRadius.all(Radius.circular(5.0)),
this.animationDuration = const Duration(milliseconds: 400),
}) : assert(progress >= 0.0 && progress <= 1.0, 'Progress must be between 0.0 and 1.0'),
super(key: key);
@override
_AnimatedProgressBarState createState() => _AnimatedProgressBarState();
}
class _AnimatedProgressBarState extends State
with SingleTickerProviderStateMixin {
// AnimationController and Animation will be initialized here
late AnimationController _animationController;
late Animation _animation;
double _currentAnimationProgress = 0.0; // Stores the progress value at the start of an animation cycle
@override
void initState() {
super.initState();
// Initialize _currentAnimationProgress with the initial progress
_currentAnimationProgress = widget.progress;
_animationController = AnimationController(
vsync: this,
duration: widget.animationDuration,
);
_animation = Tween(begin: _currentAnimationProgress, end: widget.progress).animate(_animationController)
..addListener(() {
setState(() {}); // Rebuild widget on every animation frame
});
// Start the initial animation to the provided progress
_animationController.forward(from: 0.0);
}
// ... rest of the state class
}
2. Initialize Animation Controller and Animation
In the _AnimatedProgressBarState class, we need to initialize our AnimationController and Animation within the initState method. We also use SingleTickerProviderStateMixin to provide the necessary ticker for the controller.
The _currentAnimationProgress variable is introduced to keep track of the progress value from which the next animation should start. This ensures smooth transitions even if the progress is updated multiple times.
// ... (inside _AnimatedProgressBarState class)
@override
void initState() {
super.initState();
_currentAnimationProgress = widget.progress;
_animationController = AnimationController(
vsync: this,
duration: widget.animationDuration,
);
_animation = Tween(begin: _currentAnimationProgress, end: widget.progress).animate(_animationController)
..addListener(() {
setState(() {});
});
_animationController.forward(from: 0.0); // Start the initial animation
}
// ...
3. Handle Progress Updates
When the progress property of the AnimatedProgressBar changes, we need to re-animate the bar from its current animated position to the new target progress. This is handled in the didUpdateWidget method.
// ... (inside _AnimatedProgressBarState class)
@override
void didUpdateWidget(covariant AnimatedProgressBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.progress != oldWidget.progress) {
_currentAnimationProgress = _animation.value; // Get the current animated value
_animation = Tween(begin: _currentAnimationProgress, end: widget.progress)
.animate(_animationController);
_animationController.forward(from: 0.0); // Restart animation from 0.0 (relative to current position)
}
}
// ...
4. Build the User Interface
The UI for the progress bar will consist of a Stack. The bottom layer will be a Container representing the background of the bar. On top of it, another Container wrapped in a FractionallySizedBox will represent the actual progress. The widthFactor of the FractionallySizedBox will be driven by our _animation.value.
ClipRRect is used to ensure that the progress bar respects the provided borderRadius, as Stack children might overflow without it.
// ... (inside _AnimatedProgressBarState class)
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: widget.borderRadius,
child: Container(
height: widget.height,
child: Stack(
children: [
// Background bar
Container(
decoration: BoxDecoration(
color: widget.backgroundColor,
borderRadius: widget.borderRadius, // Apply to background for consistency
),
),
// Progress bar
FractionallySizedBox(
widthFactor: _animation.value, // Use the animated value for width
child: Container(
decoration: BoxDecoration(
color: widget.progressBarColor,
borderRadius: widget.borderRadius, // Apply to progress bar
),
),
),
],
),
),
);
}
// ...
5. Clean Up Resources
It's crucial to dispose of the AnimationController when the widget is removed from the widget tree to prevent memory leaks.
// ... (inside _AnimatedProgressBarState class)
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
// ...
Full Widget Code
Here's the complete code for the AnimatedProgressBar widget:
import 'package:flutter/material.dart';
class AnimatedProgressBar extends StatefulWidget {
final double progress; // Value from 0.0 to 1.0
final Color backgroundColor;
final Color progressBarColor;
final double height;
final BorderRadiusGeometry borderRadius;
final Duration animationDuration;
const AnimatedProgressBar({
Key? key,
required this.progress,
this.backgroundColor = Colors.grey,
this.progressBarColor = Colors.blue,
this.height = 10.0,
this.borderRadius = const BorderRadius.all(Radius.circular(5.0)),
this.animationDuration = const Duration(milliseconds: 400),
}) : assert(progress >= 0.0 && progress <= 1.0, 'Progress must be between 0.0 and 1.0'),
super(key: key);
@override
_AnimatedProgressBarState createState() => _AnimatedProgressBarState();
}
class _AnimatedProgressBarState extends State
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation _animation;
double _currentAnimationProgress = 0.0;
@override
void initState() {
super.initState();
_currentAnimationProgress = widget.progress;
_animationController = AnimationController(
vsync: this,
duration: widget.animationDuration,
);
_animation = Tween(begin: _currentAnimationProgress, end: widget.progress).animate(_animationController)
..addListener(() {
setState(() {});
});
_animationController.forward(from: 0.0);
}
@override
void didUpdateWidget(covariant AnimatedProgressBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.progress != oldWidget.progress) {
_currentAnimationProgress = _animation.value;
_animation = Tween(begin: _currentAnimationProgress, end: widget.progress)
.animate(_animationController);
_animationController.forward(from: 0.0);
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: widget.borderRadius,
child: Container(
height: widget.height,
child: Stack(
children: [
// Background bar
Container(
decoration: BoxDecoration(
color: widget.backgroundColor,
borderRadius: widget.borderRadius,
),
),
// Progress bar
FractionallySizedBox(
widthFactor: _animation.value,
child: Container(
decoration: BoxDecoration(
color: widget.progressBarColor,
borderRadius: widget.borderRadius,
),
),
),
],
),
),
);
}
}
How to Use the Widget
To use the AnimatedProgressBar, simply instantiate it in your widget tree and update its progress property in a setState call. The animation will automatically handle the smooth transition.
import 'package:flutter/material.dart';
// Don't forget to import your AnimatedProgressBar widget file
// import 'path/to/animated_progress_bar.dart';
class ProgressBarDemo extends StatefulWidget {
const ProgressBarDemo({Key? key}) : super(key: key);
@override
State createState() => _ProgressBarDemoState();
}
class _ProgressBarDemoState extends State {
double _downloadProgress = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Animated Progress Bar Demo'),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedProgressBar(
progress: _downloadProgress,
height: 20.0,
progressBarColor: Colors.deepPurple,
backgroundColor: Colors.deepPurple.shade100,
borderRadius: BorderRadius.circular(10.0),
animationDuration: const Duration(milliseconds: 600),
),
const SizedBox(height: 40),
Text(
'Progress: ${(_downloadProgress * 100).toStringAsFixed(0)}%',
style: const TextStyle(fontSize: 24),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () {
setState(() {
// Simulate progress update
_downloadProgress += 0.1;
if (_downloadProgress > 1.0) _downloadProgress = 0.0;
});
},
child: const Text('Update Progress'),
),
],
),
),
);
}
}
// To run this demo, ensure your main.dart calls ProgressBarDemo:
// void main() {
// runApp(MaterialApp(home: ProgressBarDemo()));
// }
Customization Options
The AnimatedProgressBar widget offers several parameters for customization:
progress(required): The current progress value as a double between 0.0 and 1.0.backgroundColor: The color of the background portion of the bar. Defaults toColors.grey.progressBarColor: The color of the filled portion of the bar. Defaults toColors.blue.height: The height of the progress bar. Defaults to 10.0.borderRadius: The border radius for the entire bar, making it rounded or square. Defaults toRadius.circular(5.0).animationDuration: The duration of the progress animation. Defaults to 400 milliseconds.
Conclusion
Creating an animated progress bar in Flutter is an excellent way to practice and understand Flutter's animation framework. By leveraging AnimationController, Tween, and proper lifecycle management, we've built a robust and customizable widget that provides smooth visual feedback to users. You can further enhance this widget by adding text overlays, different animation curves, or even custom painting for more complex designs.