import ' dart:math ' as math ;
import ' package:flutter/material.dart ' ;
class CircularStepProgressBar extends StatelessWidget {
/// Defines if steps grow from
/// clockwise [CircularDirection.clockwise] or
/// counterclockwise [CircularDirection.counterclockwise]
final CircularDirection circularDirection ;
/// Number of steps to underline, all the steps with
/// index <= [currentStep] will have [Color] equal to
/// [selectedColor]
///
/// Only used when [customColor] is [null]
///
/// Default value: 0
final int currentStep ;
/// Total number of step of the complete indicator
final int totalSteps ;
/// Radial spacing between each step. Remember to
/// define the value in radiant units
///
/// Default value: math.pi / 20
final double padding ;
/// Height of the indicator's box container
final double ? height ;
/// Width of the indicator's box container
final double ? width ;
/// Assign a custom [Color] for each step
///
/// Takes a [int], index of the current step starting from 0, and
/// must return a [Color]
///
/// **NOTE**: If provided, it overrides
/// [selectedColor] and [unselectedColor]
final Color Function ( int ) ? customColor ;
/// [Color] of the selected steps
///
/// All the steps with index <= [currentStep]
///
/// Default value: [Colors.blue]
final Color ? selectedColor ;
/// [Color] of the unselected steps
///
/// All the steps with index between
/// [currentStep] and [totalSteps]
///
/// Default value: [Colors.grey]
final Color ? unselectedColor ;
/// The size of a single step in the indicator
///
/// Default value: 6.0
final double stepSize ;
/// Specify a custom size for selected steps
///
/// Only applicable when not custom setting (customColor, customStepSize) is defined
///
/// This value will replace the [stepSize] only for selected steps
final double ? selectedStepSize ;
/// Specify a custom size for unselected steps
///
/// Only applicable when not custom setting (customColor, customStepSize) is defined
///
/// This value will replace the [stepSize] only for unselected steps
final double ? unselectedStepSize ;
/// Assign a custom size [double] for each step
///
/// Function takes a [int], index of the current step starting from 0, and
/// a [bool], which tells if the step is selected based on [currentStep],
/// and must return a [double] size of the step
///
/// **NOTE**: If provided, it overrides [stepSize]
final double Function ( int , bool ) ? customStepSize ;
/// [Widget] contained inside the circular indicator
final Widget ? child ;
/// Height of the indicator container in case no [height] parameter
/// given and parent height is [double.infinity]
///
/// Default value: 100.0
final double fallbackHeight ;
/// Height of the indicator container in case no [width] parameter
/// given and parent height is [double.infinity]
///
/// Default value: 100.0
final double fallbackWidth ;
/// Angle in radiants in which the first step of the indicator is placed.
/// The initial value is on the top of the indicator (- math.pi / 2)
/// - 0 => TOP
/// - math.pi / 2 => LEFT
/// - math.pi => BOTTOM
/// - math.pi / 2 * 3 => RIGHT
/// - math.pi / 2 => TOP (again)
final double startingAngle ;
/// Angle in radiants which represents the size of the arc used to display the indicator.
/// It allows you to draw a semi-circle instead of a full 360° (math.pi * 2) circle.
final double arcSize ;
/// Adds rounded edges at the beginning and at the end of the circular indicator
/// given a [int], index of each step, and a [bool],
/// which tells if the step is selected based on [currentStep], and must return a
/// [bool] that tells if the edges are rounded or not
///
/// **NOTE**: For continuous circular indicators (`padding: 0`), to check if to apply
/// the rounded edges the packages uses index 0 (for the first arc painted) and
/// 1 (for the second arc painted)
///
/// ```dart
/// // Example: Add rounded edges for all the steps
/// roundedCap: (index, _) => true
/// ```
///
/// ```dart
/// // Example: Add rounded edges for the selected arc of the indicator
/// roundedCap: (index, _) => index == 0,
/// padding: 0
/// ```
final bool Function ( int , bool ) ? roundedCap ;
/// Adds a gradient color to the circular indicator
///
/// **NOTE**: If provided, it overrides [selectedColor], [unselectedColor], and [customColor]
final Gradient ? gradientColor ;
/// Removes the extra angle caused by [StrokeCap.round] when [roundedCap] is applied
final bool removeRoundedCapExtraAngle ;
const CircularStepProgressBar ( {
required this . totalSteps ,
this . child ,
this . height ,
this . width ,
this . customColor ,
this . customStepSize ,
this . selectedStepSize ,
this . unselectedStepSize ,
this . roundedCap ,
this . gradientColor ,
this . circularDirection = CircularDirection . clockwise ,
this . fallbackHeight = 100.0 ,
this . fallbackWidth = 100.0 ,
this . currentStep = 0 ,
this . selectedColor = Colors . blue ,
this . unselectedColor = Colors . grey ,
this . padding = math . pi / 20 ,
this . stepSize = 6.0 ,
this . startingAngle = 0 ,
this . arcSize = math . pi * 2 ,
this . removeRoundedCapExtraAngle = false ,
Key ? key ,
} ) : assert ( totalSteps > 0 , " Number of total steps (totalSteps) of the CircularStepProgressBar must be greater than 0 " ) ,
assert ( currentStep > = 0 , " Current step (currentStep) of the CircularStepProgressBar must be greater than or equal to 0 " ) ,
assert ( padding > = 0.0 , " Padding (padding) of the CircularStepProgressBar must be greater or equal to 0 " ) ,
super ( key: key ) ;
@ override
Widget build ( BuildContext context ) {
// Print warning when arcSize greater than math.pi * 2 which causes steps to overlap
if ( arcSize > math . pi * 2 ) print ( " WARNING (step_progress_indicator): arcSize of CircularStepProgressBar is greater than 360° (math.pi * 2), this will cause some steps to overlap! " ) ;
TextDirection textDirection = Directionality . of ( context ) ;
return LayoutBuilder (
builder: ( context , constraints ) = > SizedBox (
// Apply fallback for both height and width
// if their value is null and no parent size limit
height: height ! = null
? height
: constraints . maxHeight ! = double . infinity
? constraints . maxHeight
: fallbackHeight ,
width: width ! = null
? width
: constraints . maxWidth ! = double . infinity
? constraints . maxWidth
: fallbackWidth ,
child: CustomPaint (
painter: _CircularIndicatorPainter (
totalSteps: totalSteps ,
currentStep: currentStep ,
customColor: customColor ,
padding: padding ,
circularDirection: circularDirection ,
selectedColor: selectedColor ,
unselectedColor: unselectedColor ,
arcSize: arcSize ,
stepSize: stepSize ,
customStepSize: customStepSize ,
maxDefinedSize: maxDefinedSize ,
selectedStepSize: selectedStepSize ,
unselectedStepSize: unselectedStepSize ,
startingAngle: startingAngleTopOfIndicator ,
roundedCap: roundedCap ,
gradientColor: gradientColor ,
textDirection: textDirection ,
removeRoundedCapExtraAngle: removeRoundedCapExtraAngle ,
) ,
// Padding needed to show the indicator when child is placed on top of it
child: Padding (
padding: EdgeInsets . all ( maxDefinedSize ) ,
child: child ,
) ,
) ,
) ,
) ;
}
/// Compute the maximum possible size of the indicator between
/// [stepSize] and [customStepSize]
double get maxDefinedSize {
if ( customStepSize = = null ) {
return math . max ( stepSize , math . max ( selectedStepSize ? ? 0 , unselectedStepSize ? ? 0 ) ) ;
}
// When customSize defined, compute and return max possible size
double currentMaxSize = 0 ;
for ( int step = 0 ; step < totalSteps ; + + step ) {
// Consider max between selected and unselected case
var customSizeValue = math . max ( customStepSize ! ( step , false ) , customStepSize ! ( step , true ) ) ;
if ( customSizeValue > currentMaxSize ) {
currentMaxSize = customSizeValue ;
}
}
return currentMaxSize ;
}
/// Make [startingAngle] to top-center of indicator (0°) by default
double get startingAngleTopOfIndicator = > startingAngle - math . pi / 2 ;
}
class _CircularIndicatorPainter implements CustomPainter {
final int totalSteps ;
final int currentStep ;
final double padding ;
final Color ? selectedColor ;
final Color ? unselectedColor ;
final double stepSize ;
final double ? selectedStepSize ;
final double ? unselectedStepSize ;
final double Function ( int , bool ) ? customStepSize ;
final double maxDefinedSize ;
final Color Function ( int ) ? customColor ;
final CircularDirection circularDirection ;
final double startingAngle ;
final double arcSize ;
final bool Function ( int , bool ) ? roundedCap ;
final Gradient ? gradientColor ;
final TextDirection textDirection ;
final bool removeRoundedCapExtraAngle ;
_CircularIndicatorPainter ( {
required this . totalSteps ,
required this . circularDirection ,
required this . customColor ,
required this . currentStep ,
required this . selectedColor ,
required this . unselectedColor ,
required this . padding ,
required this . stepSize ,
required this . selectedStepSize ,
required this . unselectedStepSize ,
required this . customStepSize ,
required this . startingAngle ,
required this . arcSize ,
required this . maxDefinedSize ,
required this . roundedCap ,
required this . gradientColor ,
required this . textDirection ,
required this . removeRoundedCapExtraAngle ,
} ) ;
@ override
void paint ( Canvas canvas , Size size ) {
var w = size . width ;
var h = size . height ;
// Step length is user-defined arcSize
// divided by the total number of steps (each step same size)
var stepLength = arcSize / totalSteps ;
// Define general arc paint
Paint paint = Paint ( )
. . style = PaintingStyle . stroke
. . strokeWidth = maxDefinedSize ;
var rect = Rect . fromCenter (
// Rect created from the center of the widget
center: Offset ( w / 2 , h / 2 ) ,
// For both height and width, subtract maxDefinedSize to fit indicator inside the parent container
height: h - maxDefinedSize ,
width: w - maxDefinedSize ,
) ;
if ( gradientColor ! = null ) {
paint . shader = gradientColor ! . createShader ( rect , textDirection: textDirection ) ;
}
// Change color selected or unselected based on the circularDirection
var isClockwise = circularDirection = = CircularDirection . clockwise ;
// Make a continuous arc without rendering all the steps when possible
if ( padding = = 0 & & customColor = = null & & customStepSize = = null & & roundedCap = = null ) {
_drawContinuousArc ( canvas , paint , rect , isClockwise ) ;
} else {
_drawStepArc ( canvas , paint , rect , isClockwise , stepLength ) ;
}
}
/// Draw a series of arcs, each composing the full steps of the indicator
void _drawStepArc ( Canvas canvas , Paint paint , Rect rect , bool isClockwise , double stepLength ) {
// Draw a series of circular arcs to compose the indicator
// Starting based on startingAngle attribute
//
// When clockwise:
// - Start drawing counterclockwise so to have the selected steps on top of the unselected
int step = isClockwise ? totalSteps - 1 : 0 ;
double stepAngle = isClockwise ? startingAngle - stepLength : startingAngle ;
for ( ; isClockwise ? step > = 0 : step < totalSteps ; isClockwise ? stepAngle - = stepLength : stepAngle + = stepLength , isClockwise ? - - step : + + step ) {
// Check if the current step is selected or unselected
var isSelectedColor = _isSelectedColor ( step , isClockwise ) ;
// Size of the step
var indexStepSize = customStepSize ! = null
// Consider step index inverted when counterclockwise
? customStepSize ! ( _indexOfStep ( step , isClockwise ) , isSelectedColor )
: isSelectedColor
? selectedStepSize ? ? stepSize
: unselectedStepSize ? ? stepSize ;
// Use customColor if defined
var stepColor = customColor ! = null
// Consider step index inverted when counterclockwise
? customColor ! ( _indexOfStep ( step , isClockwise ) )
: isSelectedColor
? selectedColor !
: unselectedColor ! ;
// Apply stroke cap to each step
var hasStrokeCap = roundedCap ! = null ? roundedCap ! ( _indexOfStep ( step , isClockwise ) , isSelectedColor ) : false ;
var strokeCap = hasStrokeCap ? StrokeCap . round : StrokeCap . butt ;
// Remove extra size caused by rounded stroke cap
// https://github.com/SandroMaglione/step-progress-indicator/issues/20#issue-786114745
var extraCapSize = indexStepSize / 2 ;
var extraCapAngle = extraCapSize / ( rect . width / 2 ) ;
var extraCapRemove = hasStrokeCap & & removeRoundedCapExtraAngle ;
// Draw arc steps of the indicator
_drawArcOnCanvas (
canvas: canvas ,
rect: rect ,
startingAngle: stepAngle + ( extraCapRemove ? extraCapAngle : 0 ) ,
sweepAngle: stepLength - padding - ( extraCapRemove ? extraCapAngle * 2 : 0 ) ,
paint: paint ,
color: stepColor ,
strokeWidth: indexStepSize ,
strokeCap: strokeCap ,
) ;
}
}
/// Draw optimized continuous indicator instead of multiple steps
void _drawContinuousArc ( Canvas canvas , Paint paint , Rect rect , bool isClockwise ) {
// Compute color of the selected and unselected bars
var firstStepColor = isClockwise ? selectedColor : unselectedColor ;
var secondStepColor = ! isClockwise ? selectedColor : unselectedColor ;
// Selected and unselected step sizes if defined, otherwise use stepSize
var firstStepSize = isClockwise ? selectedStepSize ? ? stepSize : unselectedStepSize ? ? stepSize ;
var secondStepSize = ! isClockwise ? selectedStepSize ? ? stepSize : unselectedStepSize ? ? stepSize ;
// Compute length and starting angle of the selected and unselected bars
var firstArcLength = arcSize * ( currentStep / totalSteps ) ;
var secondArcLength = arcSize - firstArcLength ;
// firstArcStartingAngle = startingAngle
var secondArcStartingAngle = startingAngle + firstArcLength ;
// Apply stroke cap to both arcs
// NOTE: For continuous circular indicator, it uses 0 and 1 as index to
// apply the rounded cap
var firstArcStrokeCap = roundedCap ! = null
? isClockwise
? roundedCap ! ( 0 , true )
: roundedCap ! ( 1 , false )
: false ;
var secondArcStrokeCap = roundedCap ! = null
? isClockwise
? roundedCap ! ( 1 , false )
: roundedCap ! ( 0 , true )
: false ;
var firstCap = firstArcStrokeCap ? StrokeCap . round : StrokeCap . butt ;
var secondCap = secondArcStrokeCap ? StrokeCap . round : StrokeCap . butt ;
// When clockwise, draw the second arc first and the first on top of it
// Required when stroke cap is rounded to make the selected step on top of the unselected
if ( circularDirection = = CircularDirection . clockwise ) {
// Second arc, selected when counterclockwise, unselected otherwise
_drawArcOnCanvas (
canvas: canvas ,
rect: rect ,
paint: paint ,
startingAngle: secondArcStartingAngle ,
sweepAngle: secondArcLength ,
strokeWidth: secondStepSize ,
color: secondStepColor ! ,
strokeCap: secondCap ,
) ;
// First arc, selected when clockwise, unselected otherwise
_drawArcOnCanvas (
canvas: canvas ,
rect: rect ,
paint: paint ,
startingAngle: startingAngle ,
sweepAngle: firstArcLength ,
strokeWidth: firstStepSize ,
color: firstStepColor ! ,
strokeCap: firstCap ,
) ;
} else {
// First arc, selected when clockwise, unselected otherwise
_drawArcOnCanvas (
canvas: canvas ,
rect: rect ,
paint: paint ,
startingAngle: startingAngle ,
sweepAngle: firstArcLength ,
strokeWidth: firstStepSize ,
color: firstStepColor ! ,
strokeCap: firstCap ,
) ;
// Second arc, selected when counterclockwise, unselected otherwise
_drawArcOnCanvas (
canvas: canvas ,
rect: rect ,
paint: paint ,
startingAngle: secondArcStartingAngle ,
sweepAngle: secondArcLength ,
strokeWidth: secondStepSize ,
color: secondStepColor ! ,
strokeCap: secondCap ,
) ;
}
}
/// Draw the actual arc for a continuous indicator
void _drawArcOnCanvas ( {
required Canvas canvas ,
required Rect rect ,
required double startingAngle ,
required double sweepAngle ,
required Paint paint ,
required Color color ,
required double strokeWidth ,
required StrokeCap strokeCap ,
} ) = >
canvas . drawArc (
rect ,
startingAngle ,
sweepAngle ,
false /*isRadial*/ ,
paint
. . color = color
. . strokeWidth = strokeWidth
. . strokeCap = strokeCap ,
) ;
bool _isSelectedColor ( int step , bool isClockwise ) = > isClockwise ? step < currentStep : ( step + 1 ) > ( totalSteps - currentStep ) ;
/// Start counting indexes from the right if clockwise and on the left if counterclockwise
int _indexOfStep ( int step , bool isClockwise ) = > isClockwise ? step : totalSteps - step - 1 ;
@ override
bool shouldRepaint ( CustomPainter oldDelegate ) = > oldDelegate ! = this ;
@ override
bool hitTest ( Offset position ) = > false ;
@ override
void addListener ( listener ) { }
@ override
void removeListener ( listener ) { }
@ override
Null get semanticsBuilder = > null ;
@ override
bool shouldRebuildSemantics ( CustomPainter oldDelegate ) = > false ;
}
/// Used to define the [circularDirection] attribute of the [CircularStepProgressBar]
enum CircularDirection {
clockwise ,
counterclockwise ,
}