Last active
July 8, 2024 15:06
-
-
Save stevenosse/1c578807e1a6bbf3b0f9a6ea95ee4dda to your computer and use it in GitHub Desktop.
Draw a progress bar around a widget. This implementation extends ShapeBorder so it's usable around a Card (or any other widget expecting a ShapeBorder)
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'; | |
void main() { | |
runApp(MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
final double _progress = .5; | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
home: Scaffold( | |
body: Padding( | |
padding: EdgeInsets.all(16.0), | |
child: Column( | |
children: [ | |
const Spacer(), | |
Card( | |
shape: ProgressShapeBorder( | |
progress: _progress, | |
color: Theme.of(context).colorScheme.primary, | |
radius: 16, | |
width: 4, | |
), | |
child: Center( | |
child: Padding( | |
padding: EdgeInsets.all(32.0), | |
child: Text('${_progress * 100}%'), | |
), | |
), | |
), | |
const Spacer(), | |
] | |
) | |
) | |
), | |
); | |
} | |
} | |
class ProgressShapeBorder extends ShapeBorder { | |
final double progress; | |
final double radius; | |
final Color color; | |
final double width; | |
const ProgressShapeBorder({ | |
required this.progress, | |
required this.radius, | |
required this.color, | |
required this.width, | |
}); | |
@override | |
EdgeInsetsGeometry get dimensions => EdgeInsets.all(width); | |
@override | |
Path getInnerPath(Rect rect, {TextDirection? textDirection}) { | |
return _createPath(rect.deflate(width / 2)); | |
} | |
@override | |
Path getOuterPath(Rect rect, {TextDirection? textDirection}) { | |
return _createPath(rect.inflate(width / 2)); | |
} | |
Path _createPath(Rect rect) { | |
final path = Path(); | |
final rrect = RRect.fromRectAndRadius(rect, Radius.circular(radius)); | |
path.addRRect(rrect); | |
return path; | |
} | |
@override | |
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { | |
final paint = Paint() | |
..color = color | |
..style = PaintingStyle.stroke | |
..strokeWidth = width | |
..strokeCap = StrokeCap.round; | |
final totalLength = 2 * (rect.width + rect.height) + 4 * radius * 1.5708; // Total length including corners | |
final progressLength = totalLength * progress; | |
final path = Path(); | |
double remainingLength = progressLength; | |
// Top border | |
final topLeft = Offset(rect.left + radius, rect.top); | |
final topRight = Offset(rect.right - radius, rect.top); | |
final topLength = (topRight - topLeft).distance; | |
if (remainingLength > 0) { | |
if (remainingLength <= topLength) { | |
path.moveTo(topLeft.dx, topLeft.dy); | |
path.lineTo(topLeft.dx + remainingLength, topLeft.dy); | |
remainingLength = 0; | |
} else { | |
path.moveTo(topLeft.dx, topLeft.dy); | |
path.lineTo(topRight.dx, topRight.dy); | |
remainingLength -= topLength; | |
} | |
} | |
// Top-right corner | |
final topRightCorner = Offset(rect.right, rect.top + radius); | |
final topRightCornerLength = radius * 1.5708; | |
if (remainingLength > 0) { | |
if (remainingLength <= topRightCornerLength) { | |
path.arcToPoint( | |
Offset(rect.right, rect.top + remainingLength), | |
radius: Radius.circular(radius), | |
clockwise: true, | |
); | |
remainingLength = 0; | |
} else { | |
path.arcToPoint( | |
topRightCorner, | |
radius: Radius.circular(radius), | |
clockwise: true, | |
); | |
remainingLength -= topRightCornerLength; | |
} | |
} | |
// Right border | |
final bottomRight = Offset(rect.right, rect.bottom - radius); | |
final rightLength = (bottomRight - topRightCorner).distance; | |
if (remainingLength > 0) { | |
if (remainingLength <= rightLength) { | |
path.lineTo(bottomRight.dx, bottomRight.dy); | |
remainingLength = 0; | |
} else { | |
path.lineTo(bottomRight.dx, bottomRight.dy); | |
remainingLength -= rightLength; | |
} | |
} | |
// Bottom-right corner | |
final bottomRightCorner = Offset(rect.right - radius, rect.bottom); | |
final bottomRightCornerLength = radius * 1.5708; | |
if (remainingLength > 0) { | |
if (remainingLength <= bottomRightCornerLength) { | |
path.arcToPoint( | |
Offset(rect.right - remainingLength, rect.bottom), | |
radius: Radius.circular(radius), | |
clockwise: true, | |
); | |
remainingLength = 0; | |
} else { | |
path.arcToPoint( | |
bottomRightCorner, | |
radius: Radius.circular(radius), | |
clockwise: true, | |
); | |
remainingLength -= bottomRightCornerLength; | |
} | |
} | |
// Bottom border | |
final bottomLeft = Offset(rect.left + radius, rect.bottom); | |
final bottomLength = (bottomRightCorner - bottomLeft).distance; | |
if (remainingLength > 0) { | |
if (remainingLength <= bottomLength) { | |
path.lineTo(bottomRightCorner.dx - remainingLength, bottomRightCorner.dy); | |
remainingLength = 0; | |
} else { | |
path.lineTo(bottomLeft.dx, bottomLeft.dy); | |
remainingLength -= bottomLength; | |
} | |
} | |
// Bottom-left corner | |
final bottomLeftCorner = Offset(rect.left, rect.bottom - radius); | |
final bottomLeftCornerLength = radius * 1.5708; | |
if (remainingLength > 0) { | |
if (remainingLength <= bottomLeftCornerLength) { | |
path.arcToPoint( | |
Offset(rect.left, rect.bottom - remainingLength), | |
radius: Radius.circular(radius), | |
clockwise: true, | |
); | |
remainingLength = 0; | |
} else { | |
path.arcToPoint( | |
bottomLeftCorner, | |
radius: Radius.circular(radius), | |
clockwise: true, | |
); | |
remainingLength -= bottomLeftCornerLength; | |
} | |
} | |
// Left border | |
final topLeftCorner = Offset(rect.left, rect.top + radius); | |
final leftLength = (bottomLeftCorner - topLeftCorner).distance; | |
if (remainingLength > 0) { | |
if (remainingLength <= leftLength) { | |
path.lineTo(bottomLeftCorner.dx, bottomLeftCorner.dy - remainingLength); | |
remainingLength = 0; | |
} else { | |
path.lineTo(topLeftCorner.dx, topLeftCorner.dy); | |
remainingLength -= leftLength; | |
} | |
} | |
// Top-left corner | |
final topLeftArc = Offset(rect.left + radius, rect.top); | |
final topLeftArcLength = radius * 1.5708; | |
if (remainingLength > 0) { | |
if (remainingLength <= topLeftArcLength) { | |
path.arcToPoint( | |
Offset(rect.left + remainingLength, rect.top), | |
radius: Radius.circular(radius), | |
clockwise: true, | |
); | |
remainingLength = 0; | |
} else { | |
path.arcToPoint( | |
topLeftArc, | |
radius: Radius.circular(radius), | |
clockwise: true, | |
); | |
remainingLength -= topLeftArcLength; | |
} | |
} | |
canvas.drawPath(path, paint); | |
} | |
@override | |
ShapeBorder scale(double t) { | |
return ProgressShapeBorder( | |
progress: progress, | |
radius: radius * t, | |
color: color, | |
width: width * t, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment