本篇会作为一个系列,原文传送门:
基于Flutter版本: 2.0.3
开篇
Flutter现在已经是移动端跨端方向非常优秀的解决方案,在阿里、字节、微店等公司,它都是一个比较重要的技术方案。目前来说除了某些场景下它还存在的一些性能问题,还有缺乏好的动态化方案,其它问题基本都已经有了好的解决方式。
canvas最早是由Apple Inc.提出的,在Mac OS X webkit中创建控制板组件使用,狭义上的canvas一词本身指HTML中的canvas标签。canvas本身英文译为『画布』,广义上它在计算机领域里指的是所有使用画布的来绘制UI这种方式。在Android和iOS它都有类似的实现。
使用过Flutter的人都应该知道,Flutter是通过Skia来自行渲染UI界面的,Skia其实就是一个复杂的canvas,因为它的大部分API和设计理念跟我们所知道的canvas类似,你们可以点击这里查看Skia文档。它可以绘制任何2d图形,在Android和iOS中Skia底层都是通过OpenGL来渲染的。
作为一个『自绘性』渲染引擎,Flutter Framework提供了很多定义好的组件,还专门给Android和iOS提供了不同风格的Material
、Cupertino
组件。设计出这些组件本身,就是提供一个性能更好的渲染架构,让Flutter开发者能够用很小的学习成本也能进入到Flutter的开发。其实整个Flutter Framework
层,都是建立在一套canvas
绘制系统之上的(底层就是Skia)。只有掌握了canvas
,才能更加深入理解Flutter的绘制流程。
为了统一Flutter的开发方式,Flutter也提供了CustomPaint
组件来赋予Flutter开发的自绘性,它能让我们面对复杂的设计稿,也能绘制出相应的效果,同时也无需我们过多关注渲染时机及性能开销等问题。
本篇我先介绍使用CustomPaint
的一些基本概念
画板坐标系
canvas是2d绘制引擎,所以它的绘制是建立在一个基本的坐标系中的
如果将整个手机屏幕作为画板,Flutter将会从屏幕最左上角作为起点,向右是x轴,向左是y轴。
CustomPaint组件
我们看看CustomPaint构造函数:
const CustomPaint({
Key? key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget? child,
})
2
3
4
5
6
7
8
9
- painter:绘制的对象,是一个
CustomPainter
。它的绘制是在child之前。如果设置了child,该painter绘制的内容会被覆盖。 - foregroundPainter:绘制的对象,是一个
CustomPainter
。它的绘制是在child之后。如果设置了child,该painter绘制的内容会覆盖child。 - size: 画板大小,如果定义了child,则会以child的尺寸为准
- isComplex: 默认值是false,定义绘制内容是否复杂,如果为true,会对canvas的绘制进行一些必要的缓存来优化性能
- willChange: 默认值是false,配合isComplex使用,控制组件是否在下一帧需要重绘
- child: 子节点,可以不设置
CustomPainter
CustomPainter
是一个抽象类,其构造函数如下
const CustomPainter({ Listenable? repaint })
- repaint: 是一个
Listenable
,一般用于动画时,传入一个监听来控制canvas组件的重绘
void paint(Canvas canvas, Size size)
这个是我们定义painter时必须实现的方法,其中canvas就是提供出我们绘制的核心,size是告诉我们画板的大小(通过CustomPaint的size或者child确定)
bool shouldRepaint(covariant CustomPainter oldDelegate)
返回 true 才会进行重绘,否则就只会绘制一次。你可以通过一些条件判断来决定是否每次绘制,这样能够节约系统资源。(注:有时候不管这里返回的是false还是true,外面的变化也能导致重新绘制,这里为什么是这样,后面的文章会给出解释)
Paint
canvas提供了画板,Paint就提供了一支笔,我们可以用不同Paint在同一个canvas中进行绘制。
Paint paint = Paint()
..isAntiAlias = true
..color = Colors.pink
..blendMode = BlendMode.colorDodge
..strokeWidth = 10
..style = PaintingStyle.fill;
2
3
4
5
6
Paint类提供了很多属性,上面只是一些常用属性,下面介绍它的所有属性
- isAntiAlias: 是否抗锯齿
- color: 画笔颜色
- strokeWidth: 画笔宽度
- style: 样式
- PaintingStyle.fill 默认 填充
- PaintingStyle.stroke 线
- strokeCap: 定义画笔端点形状
- StrokeCap.butt 无形状(默认)
- StrokeCap.round 圆形
- StrokeCap.square 正方形
- strokeJoin: 定义线段交接时的形状
- StrokeJoin.miter 默认,当两条线段夹角小于30°时,
StrokeJoin.miter
将会变成StrokeJoin.bevel
- StrokeJoin.miter 默认,当两条线段夹角小于30°时,
2. StrokeJoin.bevel
- StrokeJoin.round
- strokeMiterLimit: 当
strokeJoin
为StrokeJoin.miter
时且style
为PaintingStyle.stroke
有效,用来设置连接线的长度,一般可用strokeJoin
来替换 - imageFilter: 设置模糊度
- ImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0, TileMode tileMode = TileMode.clamp}): sigmaX与sigmaY在0~10之间,数值越大越模糊
- ImageFilter.matrix 使用matrix来创建模糊度
- ImageFilter.compose 组合两个ImageFilter
- invertColors: 反转画笔颜色(跟设置的color有关)
- blendMode: 混合模式,两个形状混合时使用的模式,具体可参考blendMode,默认为
BlendMode.srcOver
- shader: 着色器
- maskFilter: 模糊蒙版滤镜,比如绘制一些阴影效果或者艺术字等
- filterQuality: 设置滤镜(如
maskFilter
或者image
)的质量 - colorFilter: 彩色矩阵滤色器,可以通过设置此属性改变画笔颜色如黑白色
Canvas与绘制无关API
save
save
操作会保存此前的所有绘制内容和canvas状态。在调用该函数之后的绘制操作和变换操作,会重新记录。当你调用restore()
之后,会把save
到restore
之间所进行的操作与之前的内容进行合并。
下面看一个例子
Paint paint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 10;
Path generatePath(double x, double y) {
Path path = new Path();
path.moveTo(x, y);
path.lineTo(x + 100, y + 100);
path.lineTo(x + 150, y + 80);
path.lineTo(x + 100, y + 200);
path.lineTo(x, y + 100);
return path;
}
canvas.drawPath(generatePath(100, 100), paint);
canvas.rotate(10 * pi / 180);
canvas.drawPath(generatePath(100, 150), paint);
canvas.drawPath(generatePath(100, 500), paint);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我使用一个函数画了三个形状一样的图形,只是它们的位置不一样,然后在第一个图形后面使用了rotate
进行旋转,当没有使用save时,下面两个图形都会发生旋转
当我使用save时
Paint paint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 10;
Path generatePath(double x, double y) {
Path path = new Path();
path.moveTo(x, y);
path.lineTo(x + 100, y + 100);
path.lineTo(x + 150, y + 80);
path.lineTo(x + 100, y + 200);
path.lineTo(x, y + 100);
return path;
}
canvas.drawPath(generatePath(100, 100), paint);
canvas.save();
canvas.rotate(10 * pi / 180);
canvas.drawPath(generatePath(100, 150), paint);
canvas.restore();
canvas.drawPath(generatePath(100, 500), paint);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
看到区别了吧,最后绘制的图形是没有跟随上面部分旋转的。使用save方法后面必须跟一个restore
,否则会抛出异常(这也很好理解,save的功能就是划出一块区域进行一些操作,所以这一块区域必须是闭合的)。
saveLayer
saveLayer
的功能跟save
类似,不过也有一点区别,saveLayer
会创建一个新的图层来进行绘制。它有以下特点:
- 因为是创建的新的图层,所以我们设置的
Paint
上的blendMode
属性会应用到两个图层之间 - 创建图层时需要传入一个绘制区域,所有的绘制只会在这个区域中,超出区域会被隐藏
- 因为是创建图层,所以会占用更多内存,有一些性能开销
- 跟
save
一样,它后面也必须跟一个restore
restore
刚刚已经说过,调用了save
或者saveLayer
之后必须调用restore
,需要说一点的是,它是可以嵌套的,举一个例子:
canvas.save(); // start 1 save
// do some thing
canvas.save(); // start 2 save
// do some thing
canvas.restore();// end 2 save
canvas.restore();// end 1 save
2
3
4
5
6
getSaveCount
通过此获取当前位置调用save多少次,最开始是1,当调用一个save
或者saveLayer
后+1
,遇到restore后-1
角度问题
Flutter中的角度是用double表示的,Flutter里面提供了pi来计算角度,公式是:
1度 = pi / 180
90度 = 90 * (pi /180)
2
在Flutter中,一般传入参数叫startAngle
的表示开始的位置,位置表示如下
sweepAngle
表示圆弧大小
总结
本篇文章介绍了我们在使用canvas前的一些需要掌握的知识,后续会介绍canvas的一些绘制能力,如有兴趣,请关注本篇文章原地址,后面会继续更新。