How to draw custom painter not in stack in flutter? - flutter

I am learning custom painter in Flutter to create my own pie chart. I draw a circle first and then draw an arc for each percentage in the list. The problem is that the arcs are in stacks. I want the arcs to draw next to the previous arc. How could I implement it? And also I want to appear their percentage in the center of each arc.
Following is my code and the screenshot of the pie chart I created:
import 'dart:math' as math;
import 'package:flutter/material.dart';
Color randomColor =
Color((math.Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
class PieChart extends CustomPainter {
List<int> percent = [50, 20, 30];
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.grey
..style = PaintingStyle.fill;
double radius = math.min(size.width / 2, size.height);
// center point of the size
Offset center = Offset(size.width / 2, size.height / 2);
// draw circle
canvas.drawCircle(center, radius, paint);
for (int i = 0; i < percent.length; i++) {
// set the angle of the arc
double arcAngle = 2 * math.pi * (percent[i] / 100);
// change random color
paint.color = randomColor =
Color((math.Random().nextDouble() * 0xFFFFFF).toInt())
.withOpacity(1.0);
// draw the arc
canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
-math.pi / 2, arcAngle, true, paint);
}
}
#override
bool shouldRepaint(PieChart oldDelegate) => false;
#override
bool shouldRebuildSemantics(PieChart oldDelegate) => false;
}
Pie chart

Related

How to round just one end of a line using CustomPainter in Flutter?

I can change both ends to square or rounded using the strokeCap property but can't figure out how to apply tailored settings for each end (ie one end rounded and one square).
How can I achieve this effect?
import 'package:flutter/material.dart';
class LinePainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var height = size.height;
var width = size.width;
var paint = Paint()
..color = Colors.red
..strokeWidth = 20
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
var path = Path();
path.moveTo(width * 0.25, height / 2);
path.lineTo(width * 0.75, height / 2);
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
class Example extends StatefulWidget {
const Example({Key? key}) : super(key: key);
#override
State<Example> createState() => _ExampleState();
}
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomPaint(
painter: LinePainter(),
child: Container(),
),
);
}
}
There is no existing PaintingStyle or StrokeCap option for setting only one cap, currently both caps are controlled the same.
If you just want the rounded cap at one end, an alternative would be to draw the line with no caps, then draw a circle overlapping the end. This would only work for solid colors though.
#override
void paint(Canvas canvas, Size size) {
var height = size.height;
var width = size.width;
var thickness = 20;
var capRadius = thickness * 0.5 ;
//Line paint, is set to stroke
var linePaint = Paint()
..color = Colors.red
..strokeWidth = thickness
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.butt; //butt is default, no caps
//Cap paint, is set to fill by default
var capPaint = Paint()
..color = Colors.red;
//Draw line
var path = Path();
path.moveTo(width * 0.25, height / 2);
path.lineTo(width * 0.75, height / 2);
canvas.drawPath(path, linePaint);
//Draw cap
canvas.drawCircle( Offset(width * 0.75, height / 2), capRadius, capPaint );
}

How to add border radius to CustomPaint Widget Flutter

I'm trying to add border radius to my custom shaped widget using Custom Paint, but I don't know how to add rounded edges to the custom shape.
I achieved the shape, but not the rounded edges.
Below is the code for the custom paint. How can I add border radius to the edges.
``
class RPSCustomPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
Paint paint0 = Paint()
..color = const Color.fromARGB(255, 33, 150, 243)
..style = PaintingStyle.stroke
..strokeWidth = 1.4900000095367432;
Path path0 = Path();
path0.moveTo(3.03, 197.85);
path0.quadraticBezierTo(0.87, 47.28, 1.9, 1.36);
path0.lineTo(207.0, 2.0);
path0.lineTo(170.24, 197.9);
path0.quadraticBezierTo(16.26, 197.13, 3.03, 197.85);
canvas.drawPath(path0, paint0);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
``
you can use drawRRect() to draw the border radius for corners of your shape.
canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - gap - smallMarkWidth - 15,gap * 8,gap + 70,gap * 5,),Radius.circular(15.0)),backgroundPaint);

How can I take an rect of arbitrary positioning, and center it in the view, and scale it using only Canvas transform operations?

I have a rect that could be positioned anywhere in the 2d coordinate space. This rect will ultimately represent the bounds of an elaborate path (map) comprised of many points. When I draw the path, I don't want the stroke to scale along with the map coordinate, so I intend to use Path.transform to scale and translate without effecting the stroke. Because of this, I want to be able to do this using only transform operations on the canvas (or transform matrix).
To simplify the problem I am using using only a rect in my current example.
I want to do two things to the rect.
Scale it by some scale value.
Center the rect in the view (based on the Size passed in the paint method).
I believe that I need to compensate for the scale when translating but I am not sure how.
Here is my complete app code, I have comments that I hope clarify my intentions.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text("Custom Painter Test"),
),
body: Container(
width: double.infinity,
height: double.infinity,
child: CustomPaint(
painter: MyCustomPainter(),
),
),
);
}
}
class MyCustomPainter extends CustomPainter {
MyCustomPainter() : super();
#override
void paint(Canvas canvas, Size size) {
var rect = Rect.fromLTRB(-14, -20 , 200.0, 80.0);
var centerX = size.width / 2;
var centerY = size.height / 2;
var centeringXOffset = centerX - rect.center.dx;
var centeringYOffset = centerY - rect.center.dy;
var scale = 2.0;
canvas.save();
canvas.translate(-rect.center.dx, -rect.center.dy); // translate the rect so it is at 0,0 to scale from center of rect
canvas.scale(scale);
canvas.translate(rect.center.dx, rect.center.dy); // translate the rect to original position
canvas.translate(centeringXOffset, centeringYOffset); // translate to center the rect
// draw the rect with border
canvas.drawRect(rect, Paint()..color = Colors.red);
var innerRect = rect.deflate(3);
canvas.drawRect(innerRect, Paint()..color = Colors.white.withOpacity(0.8));
// draw the rect center point
canvas.drawCircle(Offset(rect.center.dx,rect.center.dy) , 3.0 , Paint()..color = Colors.red);
// draw the origincal center point of the view
canvas.restore();
canvas.drawCircle(Offset(centerX , centerY) , 4.0 , Paint()..color = Colors.black.withOpacity(0.32));
}
#override
bool shouldRepaint(MyCustomPainter oldDelegate) => true;
}
when using Matrix4 api you can apply two approaches: the first one is a generic matrix multiplication so you can combine translation, scale and rotation matrices or direct matrix scaling and translating which can be somehow tricky (as you found out when scaling and translating the Canvas in the code you posted)
below code uses those two approaches: the red shape uses matrix multiplication, the orange one uses direct matrix scaling and translating (note that for your particular case you can use rectToRect2() simplified method too)
// your widget code:
final dragDetails = ValueNotifier(DragUpdateDetails(globalPosition: Offset.zero, primaryDelta: 0.0));
#override
Widget build(BuildContext context) {
return GestureDetector(
onVerticalDragUpdate: (d) => dragDetails.value = d,
child: CustomPaint(
painter: FooPainter(dragDetails),
child: Center(child: Text('move your finger up and down', textScaleFactor: 2)),
),
);
}
// custom painter code
class FooPainter extends CustomPainter {
final ValueNotifier<DragUpdateDetails> dragDetails;
FooPainter(this.dragDetails) : super(repaint: dragDetails);
double scale = 4.0;
#override
void paint(Canvas canvas, Size size) {
final painterRect = Offset.zero & size;
canvas.drawRect(painterRect, Paint()..color = Colors.black26);
final center1 = Offset(size.width / 2, size.height / 3);
final center2 = Offset(size.width / 2, size.height * 2 / 3);
canvas.drawPoints(PointMode.lines, [
Offset(0, center1.dy), Offset(size.width, center1.dy),
Offset(0, center2.dy), Offset(size.width, center2.dy),
painterRect.topCenter, painterRect.bottomCenter
], Paint()..color = Colors.black38);
final scaleFactor = pow(2, -dragDetails.value.primaryDelta / 128);
scale *= scaleFactor;
final rect = Rect.fromLTWH(14, 20, 20, 28);
final path = Path()
..fillType = PathFillType.evenOdd
..addOval(Rect.fromCircle(center: rect.bottomLeft, radius: 8))
..addRRect(RRect.fromRectAndRadius(rect, Radius.circular(3)))
..addOval(Rect.fromCircle(center: rect.center, radius: 3));
Matrix4 matrix;
Path transformedPath;
// first solution
final tranlationMatrix = _translate(center1 - rect.center);
final scaleMatrix = _scale(scale, center1);
matrix = scaleMatrix * tranlationMatrix;
transformedPath = path.transform(matrix.storage);
canvas.drawPath(transformedPath, Paint()..color = Colors.red);
canvas.drawPath(transformedPath, Paint()..style = PaintingStyle.stroke ..strokeWidth = 4 ..color = Colors.white);
// second solution
// you can also use simplified rectToRect2() that "fills" src rect into dst one
matrix = rectToRect(rect, Rect.fromCenter(center: center2, width: scale * rect.width, height: scale * rect.height));
transformedPath = path.transform(matrix.storage);
canvas.drawPath(transformedPath, Paint()..color = Colors.orange);
canvas.drawPath(transformedPath, Paint()..style = PaintingStyle.stroke ..strokeWidth = 4 ..color = Colors.white);
canvas.drawPath(path, Paint()..color = Colors.deepPurple);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Matrix4 _translate(Offset translation) {
var dx = translation.dx;
var dy = translation.dy;
return Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
}
Matrix4 _scale(double scale, Offset focalPoint) {
var dx = (1 - scale) * focalPoint.dx;
var dy = (1 - scale) * focalPoint.dy;
return Matrix4(scale, 0, 0, 0, 0, scale, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
}
/// Return a scaled and translated [Matrix4] that maps [src] to [dst] for given [fit]
/// aligned by [alignment] within [dst]
///
/// For example, if you have a [CustomPainter] with size 300 x 200 logical pixels and
/// you want to draw an expanded, centered image with size 80 x 100 you can do the following:
///
/// ```dart
/// canvas.save();
/// var matrix = sizeToRect(imageSize, Offset.zero & customPainterSize);
/// canvas.transform(matrix.storage);
/// canvas.drawImage(image, Offset.zero, Paint());
/// canvas.restore();
/// ```
///
/// and your image will be drawn inside a rect Rect.fromLTRB(70, 0, 230, 200)
Matrix4 sizeToRect(Size src, Rect dst, {BoxFit fit = BoxFit.contain, Alignment alignment = Alignment.center}) {
FittedSizes fs = applyBoxFit(fit, src, dst.size);
double scaleX = fs.destination.width / fs.source.width;
double scaleY = fs.destination.height / fs.source.height;
Size fittedSrc = Size(src.width * scaleX, src.height * scaleY);
Rect out = alignment.inscribe(fittedSrc, dst);
return Matrix4.identity()
..translate(out.left, out.top)
..scale(scaleX, scaleY);
}
/// Like [sizeToRect] but accepting a [Rect] as [src]
Matrix4 rectToRect(Rect src, Rect dst, {BoxFit fit = BoxFit.contain, Alignment alignment = Alignment.center}) {
return sizeToRect(src.size, dst, fit: fit, alignment: alignment)
..translate(-src.left, -src.top);
}
Matrix4 rectToRect2(Rect src, Rect dst) {
final scaleX = dst.width / src.width;
final scaleY = dst.height / src.height;
return Matrix4.identity()
..translate(dst.left, dst.top)
..scale(scaleX, scaleY)
..translate(-src.left, -src.top);
}
EDIT
you can further simplify rectToRect2 method to:
Matrix4 pointToPoint(double scale, Offset srcFocalPoint, Offset dstFocalPoint) {
return Matrix4.identity()
..translate(dstFocalPoint.dx, dstFocalPoint.dy)
..scale(scale)
..translate(-srcFocalPoint.dx, -srcFocalPoint.dy);
}
and use it like this:
matrix = pointToPoint(scale, rect.center, center2);

Flutter paintZigZag

Help me in drawing the zigZag line/border as show in the image at bottom. I found a zigzag paint function in flutter doc https://api.flutter.dev/flutter/painting/paintZigZag.html but not sure how to use it.
You need to use CustomPaint() Widget.
Create your own CustomerPainter, say MyPainter, and place this in your widget tree:
CustomPaint(
size: MediaQuery.of(context).size,
painter: MyPainter(),
),
The class MyPainter should extend CustomPainter and also should override the paint() method which is where you specify the path to be painted on the canvas.
So you can just put the above 'paintZigZag' code there or call it like below along with appropriate parameters.
import 'package:flutter/material.dart';
import 'dart:math' as math;
class MyPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint();
paint.color = Colors.blue;
paint.style = PaintingStyle.fill;
paintZigZag(canvas, paint, Offset(0, 100), Offset(200, 100), 100, 5);
}
void paintZigZag(Canvas canvas, Paint paint, Offset start, Offset end,
int zigs, double width) {
assert(zigs.isFinite);
assert(zigs > 0);
canvas.save();
canvas.translate(start.dx, start.dy);
end = end - start;
canvas.rotate(math.atan2(end.dy, end.dx));
final double length = end.distance;
final double spacing = length / (zigs * 2.0);
final Path path = Path()..moveTo(0.0, 0.0);
for (int index = 0; index < zigs; index += 1) {
final double x = (index * 2.0 + 1.0) * spacing;
final double y = width * ((index % 2.0) * 2.0 - 1.0);
path.lineTo(x, y);
}
path.lineTo(length, 0.0);
canvas.drawPath(path, paint);
canvas.restore();
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

How Can I Create Arc with Radius CustomPainter like this Mockup

Please tell me how can I create this Arc with Radius CustomPainter UI Component like this Mockup Design?
There must be a gap between Start & End angle.
Code:
class ArcPainter extends CustomPainter {
final double angle = 310.0;
double toAngle(double angle) => angle * pi / 180.0;
#override
void paint(Canvas canvas, Size size) {
final Offset center = Offset(size.width / 2.0, size.height / 2.0);
final double radius = size.width / 3.0;
Paint paint = Paint()
..strokeCap = StrokeCap.round
..strokeWidth = 8.0
..style = PaintingStyle.stroke
..color = Colors.pink;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
toAngle(110.0),
toAngle(angle),
true,
paint,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Problem:
Here is the current output for my Arc with Radius. The problem is that borders are showing in the arc gap. I want to remove the border in the gap.
you can create drawPath instead of drawArc
#override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final oval = Rect.fromCenter(
center: center,
width: size.width,
height: size.height,
);
var progressPath = _path(oval, 200);
var progressPaint = _paint(Color(0xFF8987F7));
var backgroundPath = _path(oval, 280);
var backgroundPaint = _paint(Color(0xFF3a329e));
canvas
..drawPath(backgroundPath, backgroundPaint)
..drawPath(progressPath, progressPaint);
}
Paint _paint(Color color) {
return Paint()
..strokeCap = StrokeCap.round
..strokeWidth = 15.0
..style = PaintingStyle.stroke
..color = color;
}
Path _path(Rect oval, double angle) {
return Path()
..addArc(
oval,
toAngle(130),
toAngle(angle),
);
}
You told it to close the gap using the center. You can tell it to not do so:
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
toAngle(110.0),
toAngle(angle),
false, // this is called "useCenter" for a reason...
paint,
);