You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
516 lines
18 KiB
Dart
516 lines
18 KiB
Dart
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!");
|
|
final 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
|
|
final 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) {
|
|
final w = size.width;
|
|
final h = size.height;
|
|
|
|
// Step length is user-defined arcSize
|
|
// divided by the total number of steps (each step same size)
|
|
final stepLength = arcSize / totalSteps;
|
|
|
|
// Define general arc paint
|
|
Paint paint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = maxDefinedSize;
|
|
|
|
final 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
|
|
final 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
|
|
final isSelectedColor = _isSelectedColor(step, isClockwise);
|
|
|
|
// Size of the step
|
|
final indexStepSize = customStepSize != null
|
|
// Consider step index inverted when counterclockwise
|
|
? customStepSize!(_indexOfStep(step, isClockwise), isSelectedColor)
|
|
: isSelectedColor
|
|
? selectedStepSize ?? stepSize
|
|
: unselectedStepSize ?? stepSize;
|
|
|
|
// Use customColor if defined
|
|
final stepColor = customColor != null
|
|
// Consider step index inverted when counterclockwise
|
|
? customColor!(_indexOfStep(step, isClockwise))
|
|
: isSelectedColor
|
|
? selectedColor!
|
|
: unselectedColor!;
|
|
|
|
// Apply stroke cap to each step
|
|
final hasStrokeCap = roundedCap != null ? roundedCap!(_indexOfStep(step, isClockwise), isSelectedColor) : false;
|
|
final 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
|
|
final extraCapSize = indexStepSize / 2;
|
|
final extraCapAngle = extraCapSize / (rect.width / 2);
|
|
final 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
|
|
final firstStepColor = isClockwise ? selectedColor : unselectedColor;
|
|
final secondStepColor = !isClockwise ? selectedColor : unselectedColor;
|
|
|
|
// Selected and unselected step sizes if defined, otherwise use stepSize
|
|
final firstStepSize = isClockwise ? selectedStepSize ?? stepSize : unselectedStepSize ?? stepSize;
|
|
final secondStepSize = !isClockwise ? selectedStepSize ?? stepSize : unselectedStepSize ?? stepSize;
|
|
|
|
// Compute length and starting angle of the selected and unselected bars
|
|
final firstArcLength = arcSize * (currentStep / totalSteps);
|
|
final secondArcLength = arcSize - firstArcLength;
|
|
|
|
// firstArcStartingAngle = startingAngle
|
|
final 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
|
|
final firstArcStrokeCap = roundedCap != null
|
|
? isClockwise
|
|
? roundedCap!(0, true)
|
|
: roundedCap!(1, false)
|
|
: false;
|
|
final secondArcStrokeCap = roundedCap != null
|
|
? isClockwise
|
|
? roundedCap!(1, false)
|
|
: roundedCap!(0, true)
|
|
: false;
|
|
final firstCap = firstArcStrokeCap ? StrokeCap.round : StrokeCap.butt;
|
|
final 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
|
|
get semanticsBuilder => null;
|
|
|
|
@override
|
|
bool shouldRebuildSemantics(CustomPainter oldDelegate) => false;
|
|
}
|
|
|
|
/// Used to define the [circularDirection] attribute of the [CircularStepProgressBar]
|
|
enum CircularDirection {
|
|
clockwise,
|
|
counterclockwise,
|
|
}
|