本文主要讲述了 Flutter 如何实现自定义 Widget 以及自定义饼形图实战,若有不当之处敬请指正。git
阅读本文大约须要6分钟。github
Flutter 官方目前已经提供不少的小部件,能够直接使用,有 Material 风格的小部件,也有 iOS 风格的小部件,还有一些布局相关的小部件。在正常开发中能知足绝大多的页面场景,可是仍有部分小部件是官方没有提供的。虽然官方没有提供完整的小部件,可是官方提供了让咱们自定义小部件的功能。canvas
在 Flutter 中自定义 Widget 经常使用的有二种方式:经过组合其余 Widget 、自绘。函数
组合其余 Widget布局
这种方式是经过拼装其余基础的 Widget 来组合成一个新的 Widget ,好比使用 Icon 和 Text 放在 Row 来组合成一个带图标功能的 Text。学习
在平时的 Flutter 中常常会使用这种方法来实现不一样的布局。ui
自绘this
若是遇到没法经过组合完成的页面UI,或者一些独特的UI,好比圆形进度条,统计图表等。这个时候最好的办法就是经过自定义 Widget 来绘画出咱们所须要的样子,在 Flutter 中提供了 CustomPainter 和 Canvas 来供咱们绘制。spa
对于复杂或者不规则的 UI ,咱们可能没法使用组合的方式完成。好比:须要一个三角形,五边形,一个折线图,一个饼形图,数字进度条等。有时候咱们能够直接让 UI 设计师直接提供图片去展现,可是有些数据是动态的或者 UI 是须要和用户交互的,这个时候使用图片可能就达不到咱们所须要的效果了,就须要咱们本身去实现绘制 UI 了。设计
几乎全部的 UI 系统都会提供一个自绘 UI 的接口,这个接口一般会提供一个 2D 的画布 Canvas,在 Canvas 内部封装了一些基础的绘制 API,咱们只须要调用相关的绘制 API 就能够绘制各类自定的图形了。
在 Flitter 中,它为咱们提供了一个 CustomPainter Widget,咱们能够结合画笔 CustomPainter 来实现自定义 Widget。
继承 CustomPainter 须要实现这个类的两个关键方法:paint
和 shouldRepaint
。在 paint 方法决定绘制什么,使用传递过来的 canvas 和 size 完成绘制,shouldRepaint 决定否须要重绘的,返回 false 表明这个 Widget 绘制完成后不须要从新绘制。
想要完成绘制仅靠 canvas 是没法完成绘制的,还须要一个画笔 paint 。
Paint _paint = Paint()
..color = Colors.red
..isAntiAlias = true
..style = PaintingStyle.fill
..strokeWidth = 12.0;
复制代码
color: 设置画笔颜色;
isAntiAlias:是否开启抗锯齿;
style:设置填充模式;
strokeWidth:设置画笔粗细
Paint 的设置有不少,可是正常开发中不会使用那么多的属性,具体的能够参考一下官方文档;
绘制点
drawPoints(PointMode pointMode, List points, Paint paint)
绘制点只须要传入PointMode枚举和 point 集合就能够了。
pointMode枚举有三个:points(点),lines(线,隔点链接),polygon(线,相邻链接)
绘制圆
canvas.drawCircle(offset, radius, paint)
绘制圆须要传入圆心 offset ,半径 radius,设置paint的填充模式能够绘制填充和不填充的圆。
绘制椭圆
drawOval(Rect rect, Paint paint)
绘制椭圆须要传入一个矩形 Rect 来肯定大小和位置。
绘制圆弧
drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
绘制圆弧须要传入的参数比较多一点,首先须要一个矩形 Rect 来肯定大小和位置,接着传入开始弧度 startAngle,多少弧度 sweepAngle,是否使用中心点绘制 useCenter。
这里须要注意的是,Android绘制 startAngle 和 sweepAngle 使用的是角度,这里使用的弧度,角度和弧度的换算为:
弧度 = 角度 * PI/180;
角度 = 弧度 * 180/PI;
绘制圆角矩形
drawRRect(RRect rrect, Paint paint)
绘制圆角矩形比较简单,只须要传入 RRect,RRect 可直接使用 fromRectAndRadius,传入矩形大小位置 Rect 和圆角大小的 Radius。
经常使用的绘制方法就这些,canvas提供了不少的绘制,能够去官方文档查看。
从图中看大体能够分为三个步骤:
if (size.width > size.height) {
radius = size.height / 3;
} else {
radius = size.width / 3;
}
line1 = radius / 3;
line2 = radius / 2;
canvas.translate(size.width / 2, size.height / 2);
Rect rect = Rect.fromLTRB(-radius, -radius, radius, radius);
复制代码
首先根据size的大小肯定咱们所要绘制的圆形的半径,这里的半径设置为宽高中较小的一边的三分之一,为何不是一半,是由于后面须要绘制线和文字,全部须要预留出来。
接着肯定绘制圆的圆心,这里直接使用 Canvas 的 translate 方法把画布移动到圆心,接着设置圆的大小和位置为: Rect.fromLTRB(-radius, -radius, radius, radius)
。
肯定了圆形的圆心大小后,咱们就须要绘制组成圆形的每个扇形,这里咱们绘制扇形须要知道扇形的大小,全部咱们须要先定义一个数据类:
abstract class BasePieEntity{
String getTitle();
double getData();
double angle;
Color getColor();
}
复制代码
定义了一个抽象类,只须要实现 getTitle、getData 和 getColor 这三个方法,具体的数据类能够根据业务需求定义,基础该基础类便可。
在接收数据的时候,须要统计出每个数据须要多大的角度:
var total = 0.0;
this.entities.forEach((e) {
total += e.getData();
});
this.entities.forEach((e) {
e.angle = e.getData() / total * 360;
});
复制代码
计算出每条数据的角度,接下来咱们只须要循环这个数据,根据数据中的角度绘制每个扇形区域便可:
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
_paint.color = entity.getColor();
canvas.drawArc(rect, (currentAngle * pi / 180), (entity.angle * pi / 180),
true, _paint);
currentAngle += entity.angle;
}
复制代码
首先绘制线分为两个部分,一部分是斜的,一部分是横线,首先咱们绘制斜线:
绘制斜线首先找到绘制线的两个坐标点,一个坐标点在扇形的中间,且点在扇形的边缘,另外一个转折点是圆心到起始点的延长线上。
首先经过角度肯定第一个点,角度为起始角度+绘制角度的二分之一,经过三角函数计算出绘制线的起始点:
// 1,计算开始坐标和转折点坐标
var startX = r * (cos((currentAngle + (angle / 2)) * (pi / 180)));
var startY = r * (sin((currentAngle + (angle / 2)) * (pi / 180)));
复制代码
同理根据延长线的大小加上半径使用三角函数便可得出转折点的坐标:
var stopX = (r + line1) * (cos((currentAngle + (angle / 2)) * (pi / 180)));
var stopY = (r + line1) * (sin((currentAngle + (angle / 2)) * (pi / 180)));
复制代码
计算完起始点和转折点须要计算终点的坐标,终点的坐标分为两种状况,一种是在圆心的左边,那横线就是向左绘制,另外一种就是在右边,横线须要向右边绘制,根据判断左右得出终点的坐标:
// 二、计算坐标在左边仍是在右边,并计算横线结束坐标
var endX;
if (stopX - startX > 0) {
endX = stopX + line2;
} else {
endX = stopX - line2;
}
复制代码
获得了起始点,转折点,和结束点的坐标,接下来须要根据相应的坐标点绘制斜线和横线便可:
// 三、绘制斜线和横线
canvas.drawLine(Offset(startX, startY), Offset(stopX, stopY), _paint);
canvas.drawLine(Offset(stopX, stopY), Offset(endX, stopY), _paint);
复制代码
绘制完线,接下来须要绘制横线上方和下方的文字,上方绘制扇形所占的百分比,下方绘制标题。
在 Flutter 中绘制文字不是使用 Canvas 绘制,而是使用画笔 TextPainter 绘制。
在 TextPainter 中能够设置文字画笔的风格和文字的属性:
// 文字画笔 风格定义
TextPainter _newVerticalAxisTextPainter(String text, Color color) {
return _textPainter
..text = TextSpan(
text: text,
style: new TextStyle(
color: color,
fontSize: 12.0,
),
);
}
复制代码
首先咱们绘制也须要计算文字开始的坐标:
// 四、绘制文字
// 绘制下方名称
// 上下间距偏移量
var offset = 4;
// 一、测量文字
var tp = _newVerticalAxisTextPainter(name, color);
tp.layout();
var w = tp.width;
// 二、计算文字坐标
var textStartX;
if (stopX - startX > 0) {
if (w > line2) {
textStartX = (stopX + offset);
} else {
textStartX = (stopX + (line2 - w) / 2);
}
} else {
if (w > line2) {
textStartX = (stopX - offset - w);
} else {
textStartX = (stopX - (line2 - w) / 2 - w);
}
}
复制代码
同理,计算出上方百分比文字的坐标:
// 绘制上方百分比,步骤同上
var per = (angle / 360.0 * 100).toStringAsFixed(2) + "%";
var tpPre = _newVerticalAxisTextPainter(per, color);
tpPre.layout();
w = tpPre.width;
var h = tpPre.height;
if (stopX - startX > 0) {
if (w > line2) {
textStartX = (stopX + offset);
} else {
textStartX = (stopX + (line2 - w) / 2);
}
} else {
if (w > line2) {
textStartX = (stopX - offset - w);
} else {
textStartX = (stopX - (line2 - w) / 2 - w);
}
}
复制代码
计算得出起始坐标,接下来绘制下方文字:
tp.paint(canvas, Offset(textStartX, stopY + offset));
复制代码
上方百分比文字:
tpPre.paint(canvas, Offset(textStartX, stopY - offset - h));
复制代码
至此,绘制一个饼形图就完成了。
完整代码奉上GitHub地址:flutter_demo ,欢迎star和fork。
到此,本文就结束了,若有不当之处敬请指正,一块儿学习探讨,谢谢🙏。