Building an Animated Step Progress Indicator Widget in Flutter
In modern user interfaces, providing clear feedback on progress through multi-step forms or workflows significantly enhances the user experience. A step progress indicator is a visual component that guides users by showing where they are in a sequence of steps, which steps are completed, and what's upcoming. Adding subtle animations to these indicators can further improve engagement and make the transitions feel more fluid and intuitive. This article will guide you through building a customizable animated step progress indicator widget in Flutter.
Understanding the Core Components
Before diving into the implementation, let's break down the essential components of our step progress indicator:
- Steps Data Model: A way to represent each step, including its title, status (completed, current, upcoming), and potentially an icon.
- Individual Step Item: A widget responsible for rendering a single step, displaying its icon/number, title, and reflecting its current status visually.
- Connector Line: A line segment connecting two adjacent steps, whose appearance might change based on the progress.
- Overall Layout: A container that arranges multiple step items and their connectors horizontally.
- Animation: Smooth visual transitions for status changes, such as color changes for step circles or width/color changes for connector lines.
Step-by-Step Implementation
1. Defining the Step Data Model
First, let's create a simple class to encapsulate the data for each step. We'll include a title and a status enum.
enum StepStatus {
completed,
current,
upcoming,
}
class StepData {
final String title;
final StepStatus status;
final IconData? icon;
StepData({required this.title, this.status = StepStatus.upcoming, this.icon});
}
2. Creating the Individual Step Item Widget
This widget will represent a single step in our progress indicator. It will display a circle (which can contain an icon or number) and the step's title. We'll use AnimatedContainer for smooth color transitions based on the step's status.
import 'package:flutter/material.dart';
// (Assuming StepData and StepStatus are defined as above)
class StepItem extends StatelessWidget {
final StepData step;
final int stepIndex;
const StepItem({
Key? key,
required this.step,
required this.stepIndex,
}) : super(key: key);
Color _getCircleColor(StepStatus status) {
switch (status) {
case StepStatus.completed:
return Colors.green;
case StepStatus.current:
return Colors.blue;
case StepStatus.upcoming:
return Colors.grey.shade400;
}
}
Color _getTextColor(StepStatus status) {
switch (status) {
case StepStatus.completed:
case StepStatus.current:
return Colors.black87;
case StepStatus.upcoming:
return Colors.grey.shade600;
}
}
Widget _getCircleContent(StepStatus status, int index, IconData? icon) {
switch (status) {
case StepStatus.completed:
return Icon(Icons.check, color: Colors.white, size: 16);
case StepStatus.current:
return Text('${index + 1}', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold));
case StepStatus.upcoming:
return Text('${index + 1}', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold));
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 30,
height: 30,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getCircleColor(step.status),
),
alignment: Alignment.center,
child: _getCircleContent(step.status, stepIndex, step.icon),
),
SizedBox(height: 8),
Container(
width: 80, // Fixed width for text to avoid overflow
child: Text(
step.title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: _getTextColor(step.status),
fontWeight: step.status == StepStatus.current ? FontWeight.bold : FontWeight.normal,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
}
3. Assembling the Step Progress Indicator Widget
Now, let's create the main widget that arranges multiple StepItem widgets and the connector lines between them. We'll use a Row to lay them out horizontally and incorporate animated connector lines.
import 'package:flutter/material.dart';
// (Assuming StepData, StepStatus, and StepItem are defined as above)
class AnimatedStepProgressIndicator extends StatelessWidget {
final List<StepData> steps;
final double connectorThickness;
final double connectorLength;
const AnimatedStepProgressIndicator({
Key? key,
required this.steps,
this.connectorThickness = 2.0,
this.connectorLength = 40.0,
}) : super(key: key);
Color _getConnectorColor(StepStatus status) {
switch (status) {
case StepStatus.completed:
return Colors.green;
case StepStatus.current:
case StepStatus.upcoming:
return Colors.grey.shade400;
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(
steps.length * 2 - 1,
(index) {
if (index.isOdd) {
// Connector line
final int prevStepIndex = (index - 1) ~/ 2;
final StepStatus connectorStatus = steps[prevStepIndex].status == StepStatus.completed
? StepStatus.completed
: StepStatus.upcoming;
return AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: connectorThickness,
width: connectorLength,
color: _getConnectorColor(connectorStatus),
margin: EdgeInsets.symmetric(horizontal: 4), // Small margin to not touch circles
);
} else {
// Step item
final int stepIndex = index ~/ 2;
return StepItem(
step: steps[stepIndex],
stepIndex: stepIndex,
);
}
},
),
);
}
}
4. Integrating Animation
We've already used AnimatedContainer within both StepItem (for circle color) and AnimatedStepProgressIndicator (for connector line color). This widget automatically animates changes to its properties over a specified duration and curve, simplifying the animation process significantly. The key is to ensure that the properties being animated (like `color` or `width`) are changed in the parent widget, which triggers the `AnimatedContainer` to animate the transition.
5. Full Example Usage
To see our animated step progress indicator in action, let's create a simple Flutter application.
import 'package:flutter/material.dart';
// (Place StepData, StepStatus, StepItem, and AnimatedStepProgressIndicator classes here)
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _currentStepIndex = 0;
List<StepData> _getSteps() {
return [
StepData(title: 'Cart', icon: Icons.shopping_cart),
StepData(title: 'Address', icon: Icons.location_on),
StepData(title: 'Payment', icon: Icons.payment),
StepData(title: 'Confirm', icon: Icons.check_circle),
].asMap().entries.map((entry) {
int index = entry.key;
StepData step = entry.value;
StepStatus status;
if (index < _currentStepIndex) {
status = StepStatus.completed;
} else if (index == _currentStepIndex) {
status = StepStatus.current;
} else {
status = StepStatus.upcoming;
}
return StepData(title: step.title, status: status, icon: step.icon);
}).toList();
}
void _nextStep() {
setState(() {
if (_currentStepIndex < _getSteps().length - 1) {
_currentStepIndex++;
} else {
// Optionally reset or show completion message
_currentStepIndex = 0; // Reset for demonstration
}
});
}
void _previousStep() {
setState(() {
if (_currentStepIndex > 0) {
_currentStepIndex--;
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Animated Step Progress',
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: Scaffold(
appBar: AppBar(
title: Text('Animated Step Progress Indicator'),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
AnimatedStepProgressIndicator(
steps: _getSteps(),
connectorLength: 60, // Adjust connector length as needed
),
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: _currentStepIndex == 0 ? null : _previousStep,
child: Text('Previous'),
),
ElevatedButton(
onPressed: _nextStep,
child: Text(_currentStepIndex == _getSteps().length - 1 ? 'Reset' : 'Next'),
),
],
),
],
),
),
),
);
}
}
void main() {
runApp(MyApp());
}
Customization and Enhancements
This basic setup provides a solid foundation. Here are ways you can further customize and enhance your widget:
- Custom Icons: Modify
_getCircleContentinStepItemto display custom icons fromStepDatainstead of numbers or checkmarks. - Theming: Pass in custom colors, text styles, and sizes as parameters to
AnimatedStepProgressIndicatorto make it more flexible. - Tappable Steps: Wrap
StepItemwith aGestureDetectorto allow users to jump to a specific step. - Orientation: Extend the widget to support vertical progress indicators.
- More Complex Animations: For more intricate animations (e.g., a "bouncing" effect when a step becomes current), consider using
TweenAnimationBuilderor explicitAnimationControllers.
Conclusion
Building an animated step progress indicator in Flutter provides a clear and engaging way to guide users through multi-step processes. By leveraging Flutter's declarative UI and built-in animation widgets like AnimatedContainer, we can create sophisticated and visually appealing components with relative ease. This widget is highly customizable and can be adapted to various design requirements, significantly improving the overall user experience of your Flutter applications.