Last active
February 13, 2025 15:28
-
-
Save tejaswini-dev-techie/84063b146c531c0c94ff346a5aa4033b to your computer and use it in GitHub Desktop.
A Flutter CustomPainter for a step-based progress indicator with a linear progress bar and circular milestones. The filled steps include checkmarks, making it ideal for onboarding flows, task completion trackers, or progress indicators in apps. The number of filled steps is dynamically adjustable.
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: 3), // Change this dynamically | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class StepperCustomPainter extends CustomPainter { | |
| final int filledRounds; | |
| StepperCustomPainter({required this.filledRounds}); | |
| @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, | |
| ); | |
| // 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, size.width * (filledRounds / 4), | |
| 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 | |
| List<double> circleOffsets = [ | |
| size.width * 0.25, | |
| size.width * 0.50, | |
| size.width * 0.75, | |
| size.width * 1.00, | |
| ]; | |
| for (int i = 0; i < 4; i++) { | |
| bool isFilled = i < filledRounds; | |
| 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 | |
| if (isFilled) { | |
| 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