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.
hmg-mohemm-flutter-app/lib/widgets/circular_step_progress_bar....

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,
}