Skip to content

Instantly share code, notes, and snippets.

@stevenosse
Last active July 8, 2024 15:06
Show Gist options
  • Save stevenosse/1c578807e1a6bbf3b0f9a6ea95ee4dda to your computer and use it in GitHub Desktop.
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)
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