Flutter 必知必会系列 —— 随心所欲的自定义绘制
Posted 冬天的毛毛雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter 必知必会系列 —— 随心所欲的自定义绘制相关的知识,希望对你有一定的参考价值。
作者:Time_sun
在 Flutter 中,framework 为我们提供了丰富的组件,一些常见的功能和样式都有组件直接提供,比如圆角、颜色、透明度、间距等等。
然而当组件中有许多设计的元素时,就需要我们拿着画笔自定义绘制了。比如下面这样的:
这个时候我们无法使用既有的组件组装成上面的效果,那我们就需要自己绘制成这样的效果。
本篇文章就告诉大家 Flutter 中怎么绘制自定义的显示内容。
绘制前的准备
绘制组件 CustomPaint
绘制一般考虑三个要素:画布(Canvers
)、画笔(Paint
)、内容(需求)。
在 android 中,我们需要自定义绘制的话,需要继承自 View,然后重写 onDraw 方法。在 Flutter 中,Widget 只是配置的概念,并没有绘制的功能。
那难道需要自定义一组:Widget、Element、RanderObject 吗? 大家不用担心。Flutter 帮我们做好了前期的准备工作,我们只需要拿着笔在画布上,画我们想要显示的内容。这个组件就是 CustomPaint
。如下图:
const CustomPaint(
Key? key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget? child,
) : assert(size != null),
assert(isComplex != null),
assert(willChange != null),
assert(painter != null || foregroundPainter != null || (!isComplex && !willChange)),
super(key: key, child: child);
CustomPainter? painter
: 是背景内容的绘制,会在 child 的下一层
CustomPainter? foregroundPainter
: 是前景内容的绘制,会在 child 的上一层
Size size
: CustomPaint 组件的尺寸,如果设置了 child 属性,那么 size 属性就会被无视,CustomPaint 的 size 就是 child 的尺寸
bool willChange
:绘制是否可能在下一帧中改变
一般情况下,我们只需要设置 painter 和 child 就够了。
绘制舞台 CustomPainter
绘制的舞台是 CustomPainter ,包含了 画布 和 画笔🖌️。CustomPainter 是抽象类,我们需要自定义子类。如下:
class MyPainter extends CustomPainter
@override
void paint(Canvas canvas, Size size)
// TODO: implement paint
@override
bool shouldRepaint(covariant CustomPainter oldDelegate)
// TODO: implement shouldRepaint
throw UnimplementedError();
主要是两个方法:
paint()
: 真正的绘制方法,提供了画布
shouldRepaint()
: 是否重绘
paint()
的入参是:画布(Canvas) 和 尺寸(Size)。画布和其他平台是一样的,都是可以直接用来绘制的实例对象。尺寸 要么是 child 属性的大小,要么是参数 size 的大小。
绘制的画布 Canvas
Canvas
可以记录图形化的操作,比如裁剪、缩放、平移、绘制等等,结合 Canvas
和 转换等 API 可以是画出更加复杂的图形。其实我也不理解,为啥用画布画画~~。
下面就开始画画了~
操作 Canvas
画布状态操作
Canvas
维护了一个状态栈,用于保存和回退状态。比如,先对画布进行保存,然后对画布尽心裁剪,那么后续所有的绘制都是在裁剪之后的画布上进行绘制。这个时候想要在原画布进行绘制,那么就需要回退到裁剪之前的状态。
注意一下:保存和回退必须成对出现。
save()
保存状态
restore()
回退状态
void paint(Canvas canvas, Size size)
canvas.save(); //第一处
canvas.clipRRect(RRect.fromRectXY(Offset.zero & (size / 2.0), 50.0, 50.0)); //第二处
canvas.drawPaint(Paint()..color = Colors.white); //第三处
canvas.restore(); //第四处
canvas.save(); //到五处
canvas.clipRRect(RRect.fromRectXY(size.center(Offset.zero) & (size / 2.0), 50.0, 50.0)); //第六处
canvas.drawPaint(new Paint()..color = Colors.redAccent); //第七处
canvas.restore(); //第八处
canvas.drawLine(Offset(0, 0), Offset(100, 100), new Paint()..color = Colors.redAccent);//第九处
第一处:对当前的画布状态进行了保存。 当前 状态A
第二处和第三处:对保存的画布进行了裁剪,画布变成了左上角的圆角矩形。在裁剪好的画布上进行了 绘制白色。
第四处:对画布进行了回退,此时画布变成了 状态A。
第五处:对画布进行了保存,当前 状态A。
第六处和第七处:对保存的画布 进行了裁剪,画布变成了右下角的圆角矩形。在裁剪好的画布上进行了 绘制红色。
第八处:对画布进行了回退。此时画布变成了 状态 A。
第九处:在 状态A 的画布上进行了,绘制直线。
以上代码的实际绘制效果:
画布变换操作
平移画布
translate(double dx, double dy) 平移画布
平移就是改变画布的原点,dx 是 x 的值,右为正,左为负。dy 是 y 的值,上为负,下为正。
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 20;
//平移之前
canvas.drawPoints(PointMode.points, [Offset(0, 0)], _paint);
canvas.translate(200, 200);
//平移之后
canvas.drawPoints(PointMode.points, [Offset(0, 0)], _paint);
以上代码的实际绘制效果:
我们看到 代码咩有变化,但是实际的原点发生了变化。
有人可能疑问,为什么左上角的小方块比较小?
因为小圆点的大小是 20*20(strokeWidth=20),但是 小圆点的中心点是 原点(0,0)。所以 第一个就是 1/4 的小圆点大小了。
缩放画布
scale(double sx, [double sy]) 缩放画布
这里值得注意一下,是将坐标系统的x、y都进行了缩放。 不仅仅是 控件的大小,连控件的位置都会发生变化。
@override
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 20;
//最起初的状态 :长宽20*20 位置在原点(100,100)
canvas.drawPoints(PointMode.points, [Offset(100, 100)], _paint);
//先扩大两倍 :长宽40*40 位置在(200,200)
canvas.scale(2, 2);
canvas.drawPoints(PointMode.points, [Offset(100, 100)], _paint);
//再扩大1.5倍 : 长宽60*60 位置在(300,300)
canvas.scale(1.5, 1.5);
canvas.drawPoints(PointMode.points, [Offset(100, 100)], _paint);
以上代码的实际绘制效果:
想象一下?既然坐标都可以变化,那么是不是轴对称就可以实现了呢?
canvas.scale(1, -1);沿X轴镜像
canvas.scale(-1, 1);沿Y轴镜像
canvas.scale(-1, -1);沿原点镜像
大家可以试试。
旋转画布
rotate(double radians) 旋转画布
注意一下:参数是弧度制。
旋转的中心是原点。注意:平移会对原点产生影响。
@override
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 5;
canvas.translate(200, 200);
canvas.drawLine(Offset(0, 0), Offset(100, 100), _paint);
//旋转默认是原点
//现在的原点是平移影响之后的(200,200)
canvas.rotate(pi / 2);
canvas.drawLine(Offset(0, 0), Offset(100, 100), _paint..color = Colors.greenAccent);
以上代码的实际绘制效果:
斜切画布
skew(double sx, double sy) 斜切
斜切相对来说很复杂,基本公式如下:
现有坐标 (x,y) ,新坐标X,Y,
X = x + sx * y
Y = y+sy * x
@override
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 5;
canvas.translate(200, 200);
canvas.drawLine(Offset(0, 0), Offset(100, 100), _paint);
//斜切
canvas.skew(0, 1);
//(0,0) 新坐标 还是(0,0)
// (100,100) 新坐标 (100,200)
// x = 100 + 0*100
// y = 100 + 1*100
canvas.drawLine(Offset(0, 0), Offset(100, 100), _paint..color = Colors.greenAccent);
以上代码的实际绘制效果:
基本绘制
限于篇幅,这一篇我们只介绍基本的绘制。更高级的绘制我们后续接着介绍。
绘制点相关
drawPoints(PointMode pointMode, List points, Paint paint)
pointMode
: 点的绘制模式
PointMode.points: **点模式** 绘制出来一个个的点
PointMode.lines: **线模式** 每两条线绘制成一条线,在线模式下,如果点的个数是奇数个,那么最后一个点不绘制。
PointMode.polygon: **线模式** 将众多的点连接成一个多边形
points
:点的位置
paint
:绘制点的画笔,画笔可以设置颜色和样式
示例代码:
@override
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 2;
canvas.translate(100, 0);
List<Offset> offsets = [
Offset(0, 0),
Offset(10, 10),
Offset(30, 30),
Offset(50, 70),
Offset(30, 70),
];
canvas.drawPoints(PointMode.points, offsets, _paint);
canvas.translate(0, 200);
//只有两个折线,最后一个点不绘制
canvas.drawPoints(PointMode.lines, offsets, _paint);
canvas.translate(0, 200);
canvas.drawPoints(PointMode.polygon, offsets, _paint);
运行效果:
绘制线相关
drawLine(Offset p1, Offset p2, Paint paint)
p1
: 起点的坐标
p2
: 终点的坐标
paint
:绘制线的画笔
示例代码:
@override
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 5;
canvas.translate(100, 0);
canvas.drawLine(Offset(10, 10), Offset(50, 70), _paint);
运行效果:
绘制矩形相关
drawRect(Rect rect, Paint paint)
Rect
: 矩形的封装类,包含了矩形的位置、矩形的大小。
Flutter提供了多种生成矩形 Rect 的方式。
根据四个点的坐标 Rect.fromLTRB(this.left, this.top, this.right, this.bottom)
根据左上角顶点坐标和宽高 Rect.fromLTWH(double left, double top, double width, double height)
根据内切圆 ,注意生成的是正方形 Rect.fromCircle( Offset center, double radius )
根据左上角和右下角对角线 Rect.fromPoints(Offset a, Offset b)
示例代码:
@override
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 5;
canvas.translate(100, 0);
//根据四个点生成
canvas.drawRect(Rect.fromLTRB(20, 100, 80, 200), _paint);
//根据定点和宽高
canvas.drawRect(Rect.fromLTWH(100, 100, 60, 100), _paint);
//根据中心和宽高
canvas.drawRect(Rect.fromCenter(center: Offset(50, 300), width: 60, height: 100), _paint);
//根据内接圆
canvas.drawRect(Rect.fromCircle(center: Offset(130, 300), radius: 20), _paint);
//根据对角线坐标
canvas.drawRect(Rect.fromPoints(Offset(20, 410), Offset(80, 510)), _paint);
运行效果:
绘制圆相关
drawCircle(Offset c, double radius, Paint paint),
c 是圆心、radius 是半径,这两个元素构成了圆形。
示例代码:
@override
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 5;
canvas.translate(100, 0);
canvas.drawCircle(Offset(20, 80), 40, _paint);
运行效果:
绘制椭圆形相关
drawOval(Rect rect, Paint paint)
rect 是椭圆的外接矩形,因此椭圆的长短轴之比就是矩形的宽高比。
示例代码:
@override
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 5;
canvas.translate(100, 0);
canvas.drawOval(Rect.fromCenter(center: Offset(20, 80), width: 20, height: 40), _paint);
canvas.drawOval(Rect.fromCenter(center: Offset(60, 80), width: 40, height: 20), _paint);
运行效果:
绘制圆弧
drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
rect :是椭圆的外接矩形,因此椭圆的长短轴之比就是矩形的宽高比。
startAngle :开始绘制圆环的角度,注意: X 轴的角度是0度, 并且必须是弧度制。
sweepAngle :圆环的角度大小,也是弧度制
useCenter :终点是否和圆心连接起来
示例代码:
@override
void paint(Canvas canvas, Size size)
Paint _paint = Paint()
..color = Colors.redAccent
..strokeWidth = 5;
canvas.translate(100, 0);
canvas.drawArc(
Rect.fromCenter(center: Offset(60, 80), width: 80, height: 40),
pi / 2,
pi / 2 + pi / 4,
true,
_paint);
canvas.drawArc(
Rect.fromCenter(center: Offset(60, 180), width: 80, height: 40),
pi / 2,
pi / 2 + pi / 4,
false,
_paint);
_paint.style = PaintingStyle.stroke;
canvas.drawArc(
Rect.fromCenter(center: Offset(60, 260), width: 80, height: 40),
pi / 2,
pi / 2 + pi / 4,
false,
_paint);
运行效果:
总结
上面就是 CustomPaint
组件的基本使用,涉及到绘制的还有贝塞尔曲线,文本、图片、动画。这些内容我们放到后面的章节继续介绍,依靠已经介绍的 API,已经可以画表格咯~。
以上是关于Flutter 必知必会系列 —— 随心所欲的自定义绘制的主要内容,如果未能解决你的问题,请参考以下文章
大数据必知必会系列——面试官问能不能手写一个spark程序?[新星计划]