
I have been using flutter quite a while now but I have encountered a problem wherein I need to create a widget that would give me a shape in the form of U. I am trying to display the timeline of a user and the time he/she spent in the day doing different activities (represented by the colors in the shape). Can you suggest me a way to solve this problem and come up with a solution that allows me to control the length of the colored segment in the shape. Thanks!
I have tried using CustomPainter in flutter but I am not able to achieve what I intend to do.
Using CustomPainter, it's achievable. I have attached two screenshots, one is demo screenshot and other is explanation. Also the code is posted here and dart pad link is shared too.

import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: MyHomePage(),
);
}
}
class MyActivity {
String startTime;
Duration duration;
Color color;
String title;
Path path = Path();
MyActivity({
required this.startTime,
required this.duration,
required this.color,
required this.title,
});
}
class MyHomePage extends StatelessWidget {
MyHomePage({super.key});
final myActivities = <MyActivity>[
MyActivity(
startTime: "9 AM",
duration: const Duration(hours: 1),
color: const Color.fromRGBO(236, 33, 43, 1),
title: "Activity A"),
MyActivity(
startTime: "10 AM",
duration: const Duration(hours: 3, minutes: 30),
color: const Color.fromRGBO(10, 162, 228, 1),
title: "Activity B"),
MyActivity(
startTime: "1.30PM",
duration: const Duration(hours: 2, minutes: 30),
color: const Color.fromRGBO(40, 177, 84, 1),
title: "Activity C"),
MyActivity(
startTime: "4 PM",
duration: const Duration(hours: 3),
color: const Color.fromRGBO(135, 4, 24, 1),
title: "Activity D"),
MyActivity(
startTime: "7 PM",
duration: const Duration(minutes: 30),
color: const Color.fromRGBO(162, 74, 161, 1),
title: "Activity E"),
];
@override
Widget build(BuildContext context) {
const inset = 24.0;
final myActivityPainter = MyActivityPainter(myActivities: myActivities);
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Flutter Demo"),
),
backgroundColor: Colors.white,
body: Container(
padding: const EdgeInsets.all(inset),
child: LayoutBuilder(builder: (context, constraints) {
return Container(
height: constraints.maxHeight,
width: constraints.maxWidth,
child: CustomPaint(
foregroundPainter: myActivityPainter,
),
);
}),
),
);
}
}
class MyActivityPainter extends CustomPainter {
List<MyActivity> myActivities;
double strokeWidth = 0;
double strokeWidthMidpoint = 0;
double xStartPoint = 0;
double yStartPoint = 0;
double yBottomEndPoint = 0;
double xEndPoint = 0;
double distBetweenXPoints = 0;
//doublerimeter of circle is 2*pi*r (our arc is semi-circle like)
double radius = 0;
double arcPerimeter = 0;
double totalPerimeter = 0;
Offset bottomCenter = const Offset(0, 0);
MyActivityPainter({required this.myActivities});
@override
void paint(Canvas canvas, Size size) {
_initializeFields(canvas, size);
_initializeActivities(canvas, size);
for (var element in myActivities) {
var paint = Paint()
..color = element.color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
var path = element.path;
canvas.drawPath(path, paint);
var textStyle = const TextStyle(
color: Colors.black,
fontSize: 30,
);
var activityTextPainter = TextPainter(
text: TextSpan(
text: element.title,
style: textStyle,
),
textDirection: TextDirection.ltr,
);
var timeTextPainter = TextPainter(
text: TextSpan(
text: element.startTime,
style: textStyle,
),
textDirection: TextDirection.ltr,
);
var bounds = element.path.getBounds();
activityTextPainter.layout(minWidth: 0, maxWidth: xEndPoint / 2);
timeTextPainter.layout(minWidth: 0, maxWidth: xEndPoint / 2);
var activityPos = Offset(
bounds.centerRight.dx - activityTextPainter.width / 2,
bounds.centerLeft.dy);
var timeTextPos = Offset(strokeWidth, bounds.topRight.dy);
// if the element is on the right side
if (bounds.right > size.width / 2) {
timeTextPos = Offset(
bounds.right - timeTextPainter.size.width - strokeWidth / 2,
bounds.topLeft.dy);
}
activityTextPainter.paint(canvas, activityPos);
timeTextPainter.paint(canvas, timeTextPos);
}
}
void _initializeFields(Canvas canvas, Size size) {
strokeWidth = size.width * 0.25;
strokeWidthMidpoint = strokeWidth / 2;
xStartPoint = strokeWidthMidpoint;
yStartPoint = 0.0;
xEndPoint = size.width - strokeWidthMidpoint;
distBetweenXPoints =
sqrt((xEndPoint - xStartPoint) * (xEndPoint - xStartPoint));
radius = distBetweenXPoints / 2;
yBottomEndPoint = size.height - radius - strokeWidth / 2;
arcPerimeter = pi * radius;
// left height + right height + bottom arc perimeter
totalPerimeter = yBottomEndPoint * 2 + arcPerimeter;
bottomCenter = Offset(size.width / 2, yBottomEndPoint);
}
void _initializeActivities(canvas, size) {
final totalDuration = myActivities.fold(0,
(previousValue, element) => previousValue + element.duration.inMinutes);
var consumedPerimeter = 0.0;
var prevOffset = Offset(xStartPoint, yStartPoint);
for (var element in myActivities) {
final minutes = element.duration.inMinutes;
if (minutes == 0) {
continue;
}
element.path = Path();
var path = element.path;
var currPerimeter = (minutes / totalDuration) * totalPerimeter;
// we cover left side, bottom(arc perimeter) and right side
while (currPerimeter > 0 && consumedPerimeter < totalPerimeter) {
if (consumedPerimeter >= yBottomEndPoint + arcPerimeter) {
// this cover cases for right side height
var availableHeight = totalPerimeter - consumedPerimeter;
availableHeight = min(currPerimeter, availableHeight);
path.moveTo(prevOffset.dx, prevOffset.dy);
path.lineTo(xEndPoint, prevOffset.dy - availableHeight);
prevOffset = Offset(xEndPoint, prevOffset.dy - availableHeight);
consumedPerimeter += availableHeight;
currPerimeter -= availableHeight;
} else if (consumedPerimeter >= yBottomEndPoint) {
// this covers bottom arc perimeter
var availablePerimeter =
(yBottomEndPoint + arcPerimeter) - consumedPerimeter;
availablePerimeter = min(currPerimeter, availablePerimeter);
path.moveTo(prevOffset.dx, prevOffset.dy);
// distance between two points
var arcPoint = findArcPoint(prevOffset.dx, prevOffset.dy,
bottomCenter.dx, bottomCenter.dy, availablePerimeter, false);
path.arcToPoint(arcPoint,
radius: Radius.circular(radius), clockwise: false);
prevOffset = Offset(arcPoint.dx, arcPoint.dy);
consumedPerimeter += availablePerimeter;
currPerimeter -= availablePerimeter;
} else {
// this cover cases for left side height
var availableHeight =
min(currPerimeter, yBottomEndPoint - consumedPerimeter);
path.moveTo(prevOffset.dx, prevOffset.dy+1);
path.lineTo(xStartPoint, consumedPerimeter + availableHeight);
prevOffset = Offset(xStartPoint, consumedPerimeter + availableHeight);
consumedPerimeter += availableHeight;
currPerimeter -= availableHeight;
}
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
// Ref:- https://math.stackexchange.com/questions/275201/how-to-find-an-end-point-of-an-arc-given-another-end-point-radius-and-arc-dire
Offset findArcPoint(double aX, aY, cX, cY, L, bool clockwise) {
var r = sqrt(pow(aX - cX, 2) + pow(aY - cY, 2));
var angle = atan2(aY - cY, aX - cX);
if (!clockwise) {
angle = angle - L / r;
} else {
angle = angle + L / r;
}
var bX = cX + r * cos(angle);
var bY = cY + r * sin(angle);
return Offset(bX, bY);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With