Last active
February 13, 2025 15:25
-
-
Save tejaswini-dev-techie/7e27f8e2eb654b09b4dac551d2090fba to your computer and use it in GitHub Desktop.
A CustomPainter implementation in Flutter to create a dynamic step-based progress indicator. The widget supports customizable step counts, filled progress, and tick marks for completed steps. It adapts to different widths and can be easily integrated into any Flutter project. Ideal for onboarding flows, step-based tracking, and progress visualiz…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/material.dart'; | |
| import 'dart:ui' as ui; | |
| void main() { | |
| runApp(const MyApp()); | |
| } | |
| class MyApp extends StatelessWidget { | |
| const MyApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| debugShowCheckedModeBanner: false, | |
| home: Scaffold( | |
| body: Center( | |
| child: CustomPaint( | |
| size: const Size(328, 12), | |
| painter: | |
| StepperCustomPainter(filledRounds: 4, totalSteps: 7), // Change dynamically | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class StepperCustomPainter extends CustomPainter { | |
| final int filledRounds; | |
| final int totalSteps; | |
| StepperCustomPainter({required this.filledRounds, required this.totalSteps}); | |
| @override | |
| void paint(Canvas canvas, Size size) { | |
| // Background progress bar | |
| Paint backgroundPaint = Paint()..style = PaintingStyle.fill; | |
| backgroundPaint.color = const Color(0xffE9E5EF); | |
| canvas.drawRRect( | |
| RRect.fromRectAndCorners( | |
| Rect.fromLTWH(0, size.height * 0.25, size.width, size.height * 0.5), | |
| bottomRight: Radius.circular(size.width * 0.009), | |
| bottomLeft: Radius.circular(size.width * 0.009), | |
| topLeft: Radius.circular(size.width * 0.009), | |
| topRight: Radius.circular(size.width * 0.009), | |
| ), | |
| backgroundPaint, | |
| ); | |
| // Calculate the width of each step dynamically based on total steps | |
| double stepWidth = size.width / (totalSteps - 1); | |
| // Filled progress bar | |
| Paint filledPaint = Paint()..style = PaintingStyle.fill; | |
| filledPaint.color = const Color(0xff2E2E54); | |
| canvas.drawRRect( | |
| RRect.fromRectAndCorners( | |
| Rect.fromLTWH(0, size.height * 0.25, stepWidth * (filledRounds - 1), // Adjust for filled rounds | |
| size.height * 0.5), | |
| bottomRight: Radius.circular(size.width * 0.009), | |
| bottomLeft: Radius.circular(size.width * 0.009), | |
| topLeft: Radius.circular(size.width * 0.009), | |
| topRight: Radius.circular(size.width * 0.009), | |
| ), | |
| filledPaint, | |
| ); | |
| // Circle positions dynamically based on totalSteps | |
| List<double> circleOffsets = []; | |
| for (int i = 0; i < totalSteps; i++) { | |
| circleOffsets.add(stepWidth * i); // Adjust position dynamically for total steps | |
| } | |
| for (int i = 0; i < totalSteps; i++) { | |
| bool isFilled = i < filledRounds; | |
| bool hasTickMark = i < filledRounds - 1; // Only previous rounds get a tick mark | |
| Paint borderPaint = Paint() | |
| ..color = isFilled ? Colors.white : const Color(0xff2E2E54) | |
| ..style = PaintingStyle.stroke | |
| ..strokeWidth = size.width * 0.003; | |
| Paint fillPaint = Paint()..style = PaintingStyle.fill; | |
| fillPaint.color = | |
| isFilled ? const Color(0xff2E2E54) : const Color(0xffE9E5EF); | |
| // Draw circle with border | |
| canvas.drawCircle( | |
| Offset(circleOffsets[i], size.height * 0.5), | |
| size.width * 0.0175, | |
| fillPaint, | |
| ); | |
| canvas.drawCircle( | |
| Offset(circleOffsets[i], size.height * 0.5), | |
| size.width * 0.0175, | |
| borderPaint, | |
| ); | |
| // Draw tick mark for filled circles except the latest filled one | |
| if (hasTickMark) { | |
| Path tickPath = Path(); | |
| double tickStartX = circleOffsets[i] - size.width * 0.006; | |
| double tickStartY = size.height * 0.52; | |
| double tickMidX = circleOffsets[i] - size.width * 0.002; | |
| double tickMidY = size.height * 0.58; | |
| double tickEndX = circleOffsets[i] + size.width * 0.006; | |
| double tickEndY = size.height * 0.42; | |
| tickPath.moveTo(tickStartX, tickStartY); | |
| tickPath.lineTo(tickMidX, tickMidY); | |
| tickPath.lineTo(tickEndX, tickEndY); | |
| Paint tickPaint = Paint() | |
| ..color = Colors.white | |
| ..style = PaintingStyle.stroke | |
| ..strokeWidth = size.width * 0.003 | |
| ..strokeCap = StrokeCap.round | |
| ..strokeJoin = StrokeJoin.round; | |
| canvas.drawPath(tickPath, tickPaint); | |
| } | |
| } | |
| } | |
| @override | |
| bool shouldRepaint(covariant CustomPainter oldDelegate) { | |
| return true; | |
| } | |
| } |
Author
tejaswini-dev-techie
commented
Feb 13, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment