Introducción
En el proceso de desarrollo, generalmente nos encontramos con algunas IU irregulares, como líneas irregulares, polígonos, gráficos estadísticos, etc., que no se pueden realizar combinando esos componentes comunes, lo que requiere que los dibujemos nosotros mismos. Se pueden dibujar varias formas manualmente usando CuntomPaint
componentes combinados con pinceles .CustomPainter
Introducción a CustomPaint
CustomPaint
Es una herencia SingleChildRenderObjectWidget
, Widget
aquí introduce principalmente varios parámetros importantes:
child
: CustomPaint
subcomponentes. painter
: Pincel, los gráficos dibujados se mostrarán en la child
parte posterior.
foregroundPainter
: Pincel de primer plano, los gráficos dibujados se mostrarán al child
frente.
size
: Tamaño del área de dibujo.
Introducción a CustomPainter
CustomPainter
CustomPainter
Es una clase abstracta que hereda de , anula paint
y métodos al personalizar una clase , y shouldRepaint
el dibujo específico está principalmente en el paint
método.
Introducción a la pintura
Hay dos parámetros principales: Canvas
: Canvas, que se puede utilizar para dibujar varios gráficos. Size
: El tamaño del área de dibujo.
void paint(Canvas canvas, Size size)
复制代码
Descripción de shouldRepaint
Se llamará a este método antes de que se vuelva a dibujar el widget para determinar cuándo se debe volver a dibujar. El shouldRepaint
retorno ture
indica que es necesario volver a dibujar y el retorno indica que false
no es necesario volver a dibujar.
bool shouldRepaint(CustomPainter oldDelegate)
复制代码
Ejemplo
Aquí demostramos el proceso general de dibujo dibujando un gráfico circular.
Uso de pintura personalizada
Primero, use CustomPaint
, dibuje el tamaño máximo del componente principal y pase custom painter
.
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size.infinite,
painter: PieChartPainter(),
);
}
复制代码
Personalizar pintor
PieChartPainter
herencia personalizadaCustomPainter
class PieChartPainters extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate != this;
}
}
复制代码
dibujar
Luego implementamos el paint
método para dibujar
@override
void paint(Canvas canvas, Size size) {
//移动到中心点
canvas.translate(size.width / 2, size.height / 2);
//绘制饼状图
_drawPie(canvas, size);
//绘制扇形分割线
_drawSpaceLine(canvas);
//绘制中心圆
_drawHole(canvas, size);
}
复制代码
dibujar un gráfico circular
Tomamos el punto medio de todo el lienzo como el punto y luego calculamos el área angular de cada sector canvas.drawArc
dibujando el sector.
void _drawPie(Canvas canvas, Size size) {
var startAngle = 0.0;
var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value);
for (var model in models) {
Paint paint = Paint()
..style = PaintingStyle.fill
..color = model.color;
var sweepAngle = model.value / sumValue * 360;
canvas.drawArc(Rect.fromCircle(radius: model.radius, center: Offset.zero),
startAngle * pi / 180, sweepAngle * pi / 180, true, paint);
//为每一个区域绘制延长线和文字
_drawLineAndText(
canvas, size, model.radius, startAngle, sweepAngle, model);
startAngle += sweepAngle;
}
}
复制代码
绘制延长线以及文本
延长线的起点为扇形区域边缘中点位置,长度为一个固定的长度,转折点坐标通过半径加这个固定长度和三角函数进行计算,然后通过转折点的位置决定横线终点的方向,而横线的长度则根据文字的宽度决定,然后通过canvas.drawLine
进行绘制直线。
文本绘制使用TextPainter.paint
进行绘制,paint
方法里面最终是通过canvas.drawParagraph
进行绘制的。
最后再在文字的前面通过canvas.drawCircle
绘制一个小圆点。
void _drawLineAndText(Canvas canvas, Size size, double radius,
double startAngle, double sweepAngle, PieChartModel model) {
var ratio = (sweepAngle / 360.0 * 100).toStringAsFixed(2);
var top = Text(model.name);
var topTextPainter = getTextPainter(top);
var bottom = Text("$ratio%");
var bottomTextPainter = getTextPainter(bottom);
// 绘制横线
// 计算开始坐标以及转折点的坐标
var startX = radius * (cos((startAngle + (sweepAngle / 2)) * (pi / 180)));
var startY = radius * (sin((startAngle + (sweepAngle / 2)) * (pi / 180)));
var firstLine = radius / 5;
var secondLine =
max(bottomTextPainter.width, topTextPainter.width) + radius / 4;
var pointX = (radius + firstLine) *
(cos((startAngle + (sweepAngle / 2)) * (pi / 180)));
var pointY = (radius + firstLine) *
(sin((startAngle + (sweepAngle / 2)) * (pi / 180)));
// 计算坐标在左边还是在右边
// 并计算横线结束坐标
// 如果结束坐标超过了绘制区域,则改变结束坐标的值
var marginOffset = 20.0; // 距离绘制边界的偏移量
var endX = 0.0;
if (pointX - startX > 0) {
endX = min(pointX + secondLine, size.width / 2 - marginOffset);
secondLine = endX - pointX;
} else {
endX = max(pointX - secondLine, -size.width / 2 + marginOffset);
secondLine = pointX - endX;
}
Paint paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = 1
..color = Colors.grey;
// 绘制延长线
canvas.drawLine(Offset(startX, startY), Offset(pointX, pointY), paint);
canvas.drawLine(Offset(pointX, pointY), Offset(endX, pointY), paint);
// 文字距离中间横线上下间距偏移量
var offset = 4;
var textWidth = bottomTextPainter.width;
var textStartX = 0.0;
textStartX =
_calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);
bottomTextPainter.paint(canvas, Offset(textStartX, pointY + offset));
textWidth = topTextPainter.width;
var textHeight = topTextPainter.height;
textStartX =
_calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);
topTextPainter.paint(canvas, Offset(textStartX, pointY - offset - textHeight));
// 绘制文字前面的小圆点
paint.color = model.color;
canvas.drawCircle(
Offset(textStartX - 8, pointY - 4 - topTextPainter.height / 2),
4,
paint);
}
复制代码
绘制扇形分割线
在绘制完扇形之后,然后在扇形的开始的那条边上绘制一条直线,起点为圆点,长度为扇形半径,终点的位置根据半径和扇形开始的那条边的角度用三角函数进行计算,然后通过canvas.drawLine
进行绘制。
void _drawSpaceLine(Canvas canvas) {
var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value);
var startAngle = 0.0;
for (var model in models) {
_drawLine(canvas, startAngle, model.radius);
startAngle += model.value / sumValue * 360;
}
}
void _drawLine(Canvas canvas, double angle, double radius) {
var endX = cos(angle * pi / 180) * radius;
var endY = sin(angle * pi / 180) * radius;
Paint paint = Paint()
..style = PaintingStyle.fill
..color = Colors.white
..strokeWidth = spaceWidth;
canvas.drawLine(Offset.zero, Offset(endX, endY), paint);
}
复制代码
绘制内部中心圆
这里可以通过传入的参数判断是否需要绘制这个圆,使用canvas.drawCircle
进行绘制一个与背景色一致的圆。
void _drawHole(Canvas canvas, Size size) {
if (isShowHole) {
holePath.reset();
Paint paint = Paint()
..style = PaintingStyle.fill
..color = Colors.white;
canvas.drawCircle(Offset.zero, holeRadius, paint);
}
}
复制代码
触摸事件处理
接下来我们来处理点击事件,当我们点击某一个扇形区域时,此扇形需要突出显示,如下图:
重写hitTest
方法
注意
这个方法的返回值决定是否响应事件。
默认情况下返回null
,事件不会向下传递,也不会进行处理; 如果返回true
则当前组件进行处理事件; 如果返回false
则当前组件不会响应点击事件,会向下一层传递;
我直接在这里处理点击事件,通过该方法传入的offset
确定点击的位置,如果点击位置是在圆形区域内并且不在中心圆内则处理事件同时判断所点击的具体是哪个扇形,反之则恢复默认状态。
@override
bool? hitTest(Offset offset) {
if (oldTapOffset.dx==offset.dx && oldTapOffset.dy==offset.dy) {
return false;
}
oldTapOffset = offset;
for (int i = 0; i < paths.length; i++) {
if (paths[i].contains(offset) &&
!holePath.contains(offset)) {
onTap?.call(i);
oldTapOffset = offset;
return true;
}
}
onTap?.call(-1);
return false;
}
复制代码
至此,我们通过onTap
向上传递出点击的是第几个扇形,然后进行处理,更新UI就可以了。
动画实现
这里通过Widget
继承ImplicitlyAnimatedWidget
来实现,ImplicitlyAnimatedWidget
是一个抽象类,继承自StatefulWidget
,既然是StatefulWidget
那肯定还有一个State
,State
继承AnimatedWidgetBaseState
(此类继承自ImplicitlyAnimatedWidgetState
),感兴趣的小伙伴可以直接去看源码
实现AnimatedWidgetBaseState
里面的forEachTween
方法,主要是用于来更新Tween的初始值。
@override
void forEachTween(TweenVisitor<dynamic>visitor) {
customPieTween = visitor(customPieTween, end, (dynamic value) {
return CustomPieTween(begin: value, end: end);
}) as CustomPieTween;
}
复制代码
自定义CustomPieTween
继承自Tween
,重写lerp
方法,对需要做动画的参数进行处理
class CustomPieTween extends Tween<List<PieChartModel>> {
CustomPieTween({List<PieChartModel>? begin, List<PieChartModel>? end})
: super(begin: begin, end: end);
@override
List<PieChartModel> lerp(double t) {
List<PieChartModel> list = [];
begin?.asMap().forEach((index, model) {
list.add(model
..radius = lerpDouble(model.radius, end?[index].radius ?? 100.0, t));
});
return list;
}
double lerpDouble(double radius, double radius2, double t) {
if (radius == radius2) {
return radius;
}
var d = (radius2 - radius) * t;
var value = radius + d;
return value;
}
}
复制代码
完整代码
感兴趣的小伙伴可以直接看源码 GitHub:chart_view