I'm using fl_chart package, and it has been helpful, but it lacks the possibility to edit the circular chart styling to make the edges more circular, so if anyone could point out to a better way to achieve this design :
What I achieved so far
Use CustomPainter
import 'dart:math';
import 'package:flutter/material.dart';
class ProgressRings extends CustomPainter {
/// From 0.0 to 1.0
final double completedPercentage;
final double circleWidth;
final List<Color> gradient;
final num gradientStartAngle;
final num gradientEndAngle;
final double progressStartAngle;
final double lengthToRemove;
ProgressRings({
this.completedPercentage,
this.circleWidth,
this.gradient,
this.gradientStartAngle = 3 * pi / 2,
this.gradientEndAngle = 4 * pi / 2,
this.progressStartAngle = 0,
this.lengthToRemove = 0,
});
#override
void paint(Canvas canvas, Size size) {
Offset center = Offset(size.width / 2, size.height / 2);
double radius = min(size.width / 2, size.height / 2);
double arcAngle = 2 * pi * (completedPercentage);
Rect boundingSquare = Rect.fromCircle(center: center, radius: radius);
paint(List<Color> colors,
{double startAngle = 0.0, double endAngle = pi * 2}) {
final Gradient gradient = SweepGradient(
startAngle: startAngle,
endAngle: endAngle,
colors: colors,
);
return Paint()
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = circleWidth
..shader = gradient.createShader(boundingSquare);
}
canvas.drawArc(
boundingSquare,
-pi / 2 + progressStartAngle,
arcAngle - lengthToRemove,
false,
paint(
gradient,
startAngle: gradientStartAngle,
endAngle: gradientEndAngle,
),
);
}
#override
bool shouldRepaint(CustomPainter painter) => true;
}
Full Source here
Video Link here
Related
I'm trying to create the quarter ring shape in the top left corner without success, any help?
I tried creating a container with border radius and then give it a border for the color, but it didn't go as I expected
You can create custom ring circle by this way
Firstly you need to Create custom class same as below
import 'dart:math' as math;
class MyCustomPainter extends CustomPainter {
final learned;
final notLearned;
final range;
final totalQuestions;
double pi = math.pi;
MyCustomPainter({this.learned, this.totalQuestions, this.notLearned, this.range});
#override
void paint(Canvas canvas, Size size) {
double strokeWidth = 7;
Rect myRect = const Offset(-50.0, -50.0) & const Size(100.0, 100.0);
var paint1 = Paint()
..color = Colors.red
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
double firstLineRadianStart = 0;
double _unAnswered = (totalQuestions - notLearned - learned) * range / totalQuestions;
double firstLineRadianEnd = (360 * _unAnswered) * math.pi / 180;
canvas.drawArc(
myRect, firstLineRadianStart, firstLineRadianEnd, false, paint1);
double _learned = (learned) * range / totalQuestions;
double secondLineRadianEnd = getRadians(_learned);
canvas.drawArc(myRect, firstLineRadianEnd, secondLineRadianEnd, false, paint1);
}
double getRadians(double value) {
return (360 * value) * pi / 180;
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Then call that class in following way
Stack(
children: [
//Your body widget will be here
CustomPaint(
painter: MyCustomPainter(
totalQuestions: 300,
learned: 75,
notLearned: 75,
range: 10),
)
],
),
It will look like this
I want to create something like that:
Want to achieve
I have achieved this:
Done up until now
I am struggling to add just vertical line at state of this circular progress bar just like the line at trailing.
import 'dart:math';
import 'package:flutter/material.dart';
class LoaderPaint extends CustomPainter {
final double percentage;
LoaderPaint({
required this.percentage,
});
deg2Rand(double deg) => deg * pi / 180;
#override
void paint(Canvas canvas, Size size) {
final midOffset = Offset(size.width / 2, size.height / 2);
final paint = Paint()
..strokeCap = StrokeCap.round
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawLine(
Offset(midOffset.dy, 10),
Offset(midOffset.dy,-10),
paint,
);
canvas.drawArc(
Rect.fromCenter(center: midOffset, width: size.width, height: size.height),
deg2Rand(-90),
deg2Rand(360 * percentage),
false,
paint,
);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Calculate the end position of the line by using the formula found in this stackoverflow answer. Since we start the angle at -90 degrees we must take that away from the sweepAngle.
Calculate the points 10 units before and after the end position of the line using the formula in this answer, plugging in the center of the circle and the end position of the line.
Draw the line with canvas.drawLine
Here is what your updated LoaderPaint class looks like with these changes:
class LoaderPaint extends CustomPainter {
final double percentage;
const LoaderPaint({
required this.percentage,
});
deg2Rand(double deg) => deg * pi / 180;
#override
void paint(Canvas canvas, Size size) {
final radius = size.width / 2;
final sweepAngle = deg2Rand(360 * percentage);
final theta = deg2Rand(-90) + sweepAngle;
final midOffset = Offset(radius, radius);
final endOffset = Offset(radius + radius * cos(theta), radius + radius * sin(theta));
final midEndDiff = sqrt(pow(endOffset.dx - midOffset.dx, 2) + pow(endOffset.dy - midOffset.dy, 2));
final paint = Paint()
..strokeCap = StrokeCap.round
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawLine(
Offset(midOffset.dy, 10),
Offset(midOffset.dy,-10),
paint,
);
canvas.drawArc(
Rect.fromCenter(center: midOffset, width: size.width, height: size.height),
deg2Rand(-90),
sweepAngle,
false,
paint,
);
canvas.drawLine(
Offset(endOffset.dx + (10/midEndDiff) * (endOffset.dx - midOffset.dx), endOffset.dy + (10/midEndDiff) * (endOffset.dy - midOffset.dy)),
Offset(endOffset.dx - (10/midEndDiff) * (endOffset.dx - midOffset.dx), endOffset.dy - (10/midEndDiff) * (endOffset.dy - midOffset.dy)),
paint,
);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
This dartpad shows it working. You can easily change the percentage to see it work at all angles.
You must bear in mind this will only work if the width and height of the CustomPaint widget are exactly the same, however your example without the end cap would also break if the width and height were different.
I want to make a data wheel.
This is what it looks like :
My problem:
I want all the text that is between 181 degrees and 360 degrees, i.e. the left part of the circle, to be flipped 180 degrees so that the reading direction is easier.
My code:
class CanvasPainter extends CustomPainter
{
List<AreaEntity> areas;
CanvasPainter(this.areas)
: dotsPerRing = areas.length;
final int dotsPerRing;
final double dotRadius = 6;
#override
void paint(Canvas canvas, Size size) {
// General variable
final Offset ringCenter = size.center(Offset.zero);
final double centerCircleRadius = size.width / 6;
final double deltaAngle = 2 * pi / dotsPerRing;
// Main Circle
_drawBigCircle(
canvas: canvas,
offset: ringCenter,
radius: centerCircleRadius,
);
_drawDots(
canvas: canvas,
offset: ringCenter,
radius: centerCircleRadius,
deltaAngle: deltaAngle
);
_drawTextDot(
canvas: canvas,
size: size,
offset: ringCenter,
radius: centerCircleRadius,
deltaAngle: deltaAngle,
areas: areas
);
}
void _drawCenterCircle({
required Canvas canvas,
required Offset offset,
required double radius
}){
Paint outCirclePaint = Paint()
..color = AppColors.kcolor_bleu
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawCircle(offset, 20, outCirclePaint);
Paint inCirclePaint = Paint()
..color = AppColors.kcolor_bleu
..strokeCap = StrokeCap.round;
canvas.drawCircle(offset, 5, inCirclePaint);
}
void _drawBigCircle({
required Canvas canvas,
required Offset offset,
required double radius
}){
Paint defaultCirclePaint = Paint()
..color = AppColors.kBg_light
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 1;
canvas.drawCircle(offset, radius, defaultCirclePaint);
}
void _drawDots({
required Canvas canvas,
required Offset offset,
required double radius,
required double deltaAngle
}){
// Dot
final Paint dotBackground = Paint()
..color = AppColors.kBg_normal
..strokeCap = StrokeCap.round;
final Paint dotPaint = Paint()
..color = AppColors.kFont_grey
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 3;
// Draw a single ring
Path dotCirclePath = Path();
for(int i = 0; i < dotsPerRing; i++){
dotCirclePath.addOval(
Rect.fromCircle(
center: offset + _rotateVector(
Offset(0, radius),
deltaAngle * i
),
radius: dotRadius
)
);
}
canvas.drawPath(dotCirclePath, dotBackground);
canvas.drawPath(dotCirclePath, dotPaint);
}
void _drawTextDot({
required Canvas canvas,
required Size size,
required Offset offset,
required double radius,
required double deltaAngle,
required List<AreaEntity> areas
}){
areas.forEach((m) {
double w3 = deltaAngle * m.id;
drawTextSlant(canvas, offset, radius, w3, m.text, TextStyle(
color: Colors.white,
fontSize: 8.0,
fontWeight: FontWeight.bold,
));
});
}
void drawTextSlant(
Canvas canvas,
Offset arcCenter,
double radius,
double w,
String text,
TextStyle style
){
final pos = Offset(radius, 0);
canvas.save();
canvas.translate(arcCenter.dx, arcCenter.dy);
canvas.rotate(w - degreeToRadian(90));
if(radianToDegree(w) > 180){
// => how to reverse text ??? -------------------------------------------------
}
final tp = measureText(canvas, text, style);
final ww = tp.height;
tp.paint(canvas, pos + Offset(15 , -ww / 2.0));
canvas.restore();
}
TextPainter measureText(Canvas canvas, String text, TextStyle style)
{
final textSpan = TextSpan(text: text, style: style);
final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr);
textPainter.layout(minWidth: 0, maxWidth: double.maxFinite);
return textPainter;
}
Offset _rotateVector(Offset vector, double angleInRadians)
{
return Offset(
(cos(angleInRadians) * vector.dx) - (sin(angleInRadians) * vector.dy),
(sin(angleInRadians) * vector.dx) - (cos(angleInRadians) * vector.dy)
);
}
double degreeToRadian(double degree) {
return degree * pi / 180;
}
double radianToDegree(double radian) {
return radian * 180 / pi;
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
I'm new to canvas so if you have any suggestions to improve my code I'll gladly accept the advice
Any guidance on the best way to accomplish this would be appreciated.
EDIT : I succeeded I edit the code
void _drawTextDot({
required Canvas canvas,
required Size size,
required Offset offset,
required double radius,
required double deltaAngle,
required List<PaxsAreaEntity> areas
}){
areas.forEach((m) {
double w3 = deltaAngle * m.id;
drawTextSlant(canvas, offset, radius, w3, m.text, TextStyle(
color: AppColors.kFont_grey,
fontSize: 6.0,
fontWeight: FontWeight.bold,
), size);
});
}
void drawTextSlant(
Canvas canvas,
Offset arcCenter,
double radius,
double w,
String text,
TextStyle style,
Size size
){
double distanceFromDot = 20;
final pos = Offset(0, 0);
canvas.save();
canvas.translate(size.width / 2, size.height / 2);
canvas.rotate(degreeToRadian(-90));
canvas.translate(radius * cos(w), radius * sin(w));
canvas.rotate(w);
final tp = measureText(canvas, text, style);
final ww = tp.height;
if(radianToDegree(w) > 180){
canvas.rotate(degreeToRadian(180));
canvas.translate(-tp.width, 0);
distanceFromDot = distanceFromDot * -1;
}
tp.paint(canvas, pos + Offset(distanceFromDot , -ww / 2.0));
canvas.restore();
}
I'm learning about the custom painter in Flutter and have been working on creating a compass which I can animate. The way I have gone about it is to use a loop to spit out x,y offset coordinates as you can see in the code below. I am running the code block 3 times, one with 360 cycles (1 deg), one with 16 cycles (22.5 deg) and one with 4 cycles (90 deg). When I render the custom painter in debug mode, I don't have an issue with performance but I'm wondering what other peoples opinions are. Would creating an image like this then animating it be too taxing on the computer?
class Compass extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paintStrokeThin = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = Colors.black;
final paintStrokeNormal = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.5
..color = Colors.black;
final paintStrokeThick = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3.0
..color = Colors.black;
createCompass(
outerDistanceFromEdge: 10.0,
innerDistanceFromEdge: 15.0,
noOfPoints: 360,
paint: paintStrokeThin,
size: size,
canvas: canvas,
);
createCompass(
outerDistanceFromEdge: 5.0,
innerDistanceFromEdge: 15.0,
noOfPoints: 16,
paint: paintStrokeNormal,
size: size,
canvas: canvas,
);
createCompass(
outerDistanceFromEdge: 0.0,
innerDistanceFromEdge: 15.0,
noOfPoints: 4,
paint: paintStrokeThick,
size: size,
canvas: canvas,
);
}
void createCompass(
{double outerDistanceFromEdge,
double innerDistanceFromEdge,
int noOfPoints,
Paint paint,
Canvas canvas,
Size size}) {
final _width = size.width;
final _height = size.height;
final _interval = (360 / noOfPoints) * (pi / 180);
for (var i = 1; i <= noOfPoints; i++) {
var rad = i * _interval;
var x1 = (0.5 * _width - outerDistanceFromEdge) * sin(rad) + 0.5 * _width;
var x2 =
(0.5 * _width - innerDistanceFromEdge) * sin(rad) + 0.5 * _height;
var y1 =
(0.5 * _height - outerDistanceFromEdge) * cos(rad) + 0.5 * _width;
var y2 =
(0.5 * _height - innerDistanceFromEdge) * cos(rad) + 0.5 * _height;
canvas.drawLine(Offset(x1, y1), Offset(x2, y2), paint);
}
}
#override
bool shouldRepaint(Compass oldDelegate) => false;
}
Like any programming language, loops are expensive functions to call, especially when rendering on the UI (animations, widgets, etc). I suggest you open up your performance overlay and check how expensive rendering that widget is.
A simple but KEY rule is to MINIMISE THE USE OF EXPENSIVE WIDGETS, so consider if it is really necessary to run the code block 3 times or to animate.
Check out this long but important video from the fluter team.
Finally, as you asked for opinions complex animations for production apps is NOT a great idea, especially if you are working alone!
Setting paint color dynamically for CustomPainter's constructor not working.
lines_painter.dart
class LinesPainter extends CustomPainter {
final double lineHeight = 8;
final int maxLines = 60;
final Color customColor;
LinesPainter(this.customColor);
#override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
canvas.save();
final Paint linePainter = Paint()
..color = customColor
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
final radius = size.width / 2;
List.generate(maxLines, (i) {
var newRadius = (i % 5 == 0) ? radius - 15 : radius - 5;
canvas.drawLine(Offset(0, radius), Offset(0, newRadius), linePainter);
canvas.rotate(2 * pi / maxLines);
});
canvas.restore();
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
utils.dart
class Utils{
List<Color> getColorsArray (){
return [
Color(0xff5733),
Color(0xc70039),
Color(0x900c3e),
Color(0x571845),
Color(0x251e3e),
Color(0x051e3e),
];
}
}
Below code should paint Round shape with lines
LinesPainter(Utils().getColorsArray()[0])
Expected result:
Current result:
As #pskink mentioned in comments I have read the documentation and i got the idea that I am missing the Alpha value in hex code.
Make a change in utils.dart file like below and it works fine for me.
class Utils{
List<Color> getColorsArray (){
return [
Color(0xffff5733),
Color(0xffc70039),
Color(0xff900c3e),
Color(0xff571845),
Color(0xff251e3e),
Color(0xff051e3e),
];
}
}