From b38f4287b6e746a41b6f214cd789314425fcbd93 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Sun, 26 Dec 2021 16:32:47 +0300 Subject: [PATCH] today attendance progress added(cont) --- lib/classes/colors.dart | 3 + lib/config/routes.dart | 5 +- lib/extensions/string_extensions.dart | 14 +- lib/ui/{ => landing}/dashboard.dart | 7 +- lib/ui/landing/today_attendance_screen.dart | 144 ++++++ lib/widgets/circular_step_progress_bar.dart | 515 ++++++++++++++++++++ 6 files changed, 683 insertions(+), 5 deletions(-) rename lib/ui/{ => landing}/dashboard.dart (98%) create mode 100644 lib/ui/landing/today_attendance_screen.dart create mode 100644 lib/widgets/circular_step_progress_bar.dart diff --git a/lib/classes/colors.dart b/lib/classes/colors.dart index e8b8b2a..20e6cc1 100644 --- a/lib/classes/colors.dart +++ b/lib/classes/colors.dart @@ -9,10 +9,13 @@ class MyColors { static const Color backgroundColor = Color(0xffF8F8F8); static const Color grey57Color = Color(0xff575757); static const Color grey77Color = Color(0xff777777); + static const Color grey70Color = Color(0xff707070); + static const Color greyACColor = Color(0xffACACAC); static const Color grey98Color = Color(0xff989898); static const Color lightGreyEFColor = Color(0xffEFEFEF); static const Color lightGreyEDColor = Color(0xffEDEDED); static const Color darkWhiteColor = Color(0xffE0E0E0); static const Color redColor = Color(0xffD02127); static const Color yellowColor = Color(0xffF4E31C); + static const Color backgroundBlackColor = Color(0xff202529); } diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 0224e73..ac167c6 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:mohem_flutter_app/ui/dashboard.dart'; +import 'package:mohem_flutter_app/ui/landing/dashboard.dart'; +import 'package:mohem_flutter_app/ui/landing/today_attendance_screen.dart'; import 'package:mohem_flutter_app/ui/login/login_screen.dart'; import 'package:mohem_flutter_app/ui/login/new_password_screen.dart'; import 'package:mohem_flutter_app/ui/login/verify_login_screen.dart'; @@ -13,6 +14,7 @@ class AppRoutes { static const String newPassword = "/newPassword"; static const String loginVerification = "/loginVerification"; static const String dashboard = "/dashboard"; + static const String todayAttendance = "/todayAttendance"; static const String initialRoute = login; static final Map routes = { @@ -20,5 +22,6 @@ class AppRoutes { verifyLogin: (context) => VerifyLoginScreen(), dashboard: (context) => Dashboard(), newPassword: (context) => NewPasswordScreen(), + todayAttendance: (context) => TodayAttendanceScreen(), }; } diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index d77bbff..f4ea9b1 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -47,9 +47,19 @@ extension EmailValidator on String { style: TextStyle(color: color ?? MyColors.darkTextColor, fontSize: 16, letterSpacing: -0.64, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), ); - Widget toText24({bool isBold = false}) => Text( + Widget toText22({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(height: 1, color: color ?? MyColors.darkTextColor, fontSize: 22, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toText24({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(height: 23 / 24, color: color ?? MyColors.darkTextColor, fontSize: 24, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toText32({Color? color, bool isBold = false}) => Text( this, - style: TextStyle(height: 23 / 24, color: MyColors.darkTextColor, fontSize: 24, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + style: TextStyle(height: 32 / 32, color: color ?? MyColors.darkTextColor, fontSize: 32, letterSpacing: -1.92, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), ); bool isValidEmail() { diff --git a/lib/ui/dashboard.dart b/lib/ui/landing/dashboard.dart similarity index 98% rename from lib/ui/dashboard.dart rename to lib/ui/landing/dashboard.dart index 2c9cf50..568ba30 100644 --- a/lib/ui/dashboard.dart +++ b/lib/ui/landing/dashboard.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; @@ -171,7 +172,9 @@ class _DashboardState extends State { ), ], ), - ), + ).onPress(() { + Navigator.pushNamed(context, AppRoutes.todayAttendance); + }), ), ), 9.width, @@ -346,7 +349,7 @@ class _DashboardState extends State { ), ), 4.height, - Expanded(child: namesD[6 % (index + 1)].toText12(isCenter: true,maxLine: 2)), + Expanded(child: namesD[6 % (index + 1)].toText12(isCenter: true, maxLine: 2)), ], ), ); diff --git a/lib/ui/landing/today_attendance_screen.dart b/lib/ui/landing/today_attendance_screen.dart new file mode 100644 index 0000000..d58fc4c --- /dev/null +++ b/lib/ui/landing/today_attendance_screen.dart @@ -0,0 +1,144 @@ +import 'package:easy_localization/src/public_ext.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/circular_step_progress_bar.dart'; + +class TodayAttendanceScreen extends StatefulWidget { + TodayAttendanceScreen({Key? key}) : super(key: key); + + @override + _TodayAttendanceScreenState createState() { + return _TodayAttendanceScreenState(); + } +} + +class _TodayAttendanceScreenState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: MyColors.backgroundBlackColor, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + ), + backgroundColor: Colors.white, + body: ListView( + children: [ + Container( + color: MyColors.backgroundBlackColor, + padding: EdgeInsets.only(left: 21, right: 21, bottom: 21), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "June 13, 2021".toText24(isBold: true, color: Colors.white), + LocaleKeys.timeLeftToday.tr().toText16(color: Color(0xffACACAC)), + 21.height, + Center( + child: CircularStepProgressBar( + totalSteps: 16 * 4, + currentStep: 16, + width: 216, + height: 216, + selectedColor: MyColors.gradiantEndColor, + unselectedColor: MyColors.grey70Color, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + "08:58:15".toText32(color: Colors.white, isBold: true), + 19.height, + "Shift Time".tr().toText12(color: MyColors.greyACColor), + "08:00 - 17:00".toText22(color: Colors.white, isBold: true), + ], + ), + ), + ), + ), + ], + ), + ), + Container( + color: MyColors.backgroundBlackColor, + child: Stack( + children: [ + Container( + height: 187, + padding: EdgeInsets.only(left: 31, right: 31, top: 31, bottom: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), + gradient: const LinearGradient(transform: GradientRotation(.64), begin: Alignment.topRight, end: Alignment.bottomRight, colors: [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ]), + ), + child: Column( + children: [ + Row( + children: [commonStatusView("Check In", "09:27"), commonStatusView("Check Out", "- - : - -")], + ), + 21.height, + Row( + children: [commonStatusView("Late In", "00:27"), commonStatusView("Regular", "08:00")], + ), + ], + ), + ), + Container( + width: double.infinity, + decoration: BoxDecoration(borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), color: Colors.white), + margin: EdgeInsets.only(top: 187 - 31), + padding: EdgeInsets.only(left: 21, right: 21, top: 24, bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: ["Mark".tr().toText12(), "Attendance".tr().toText24(), "Select the method to mark the attendance".tr().toText12(color: Color(0xff535353)), 24.height], + ), + ), + // Positioned( + // top: 187 - 21, + // child: Container( + // padding: EdgeInsets.only(left: 31, right: 31, top: 31, bottom: 16), + // decoration: BoxDecoration(borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), color: Colors.white), + // child: Column( + // children: [ + // Row( + // children: [commonStatusView("Check In", "09:27"), commonStatusView("Check Out", "- - : - -")], + // ), + // 21.height, + // Row( + // children: [commonStatusView("Late In", "00:27"), commonStatusView("Regular", "08:00")], + // ), + // ], + // ), + // ), + // ), + ], + ), + ) + ], + ), + ); + } + + Widget commonStatusView(String title, String time) => Expanded( + child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + title.toText12(color: Colors.white), + time.toText22(color: Colors.white, isBold: true), + ]), + ); +} diff --git a/lib/widgets/circular_step_progress_bar.dart b/lib/widgets/circular_step_progress_bar.dart new file mode 100644 index 0000000..75724c8 --- /dev/null +++ b/lib/widgets/circular_step_progress_bar.dart @@ -0,0 +1,515 @@ +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, +}