Flutter Canvas学习之基础知识

2,048 阅读7分钟

原文传送门:

基于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提供了不同风格的MaterialCupertino组件。设计出这些组件本身,就是提供一个性能更好的渲染架构,让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,
})
  • 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;

Paint类提供了很多属性,上面只是一些常用属性,下面介绍它的所有属性

  • isAntiAlias: 是否抗锯齿
  • color: 画笔颜色
  • strokeWidth: 画笔宽度
  • style: 样式
    • PaintingStyle.fill 默认 填充
    • PaintingStyle.stroke 线
  • strokeCap: 定义画笔端点形状
    • StrokeCap.butt 无形状(默认)
    • StrokeCap.round 圆形
    • StrokeCap.square 正方形
  • strokeJoin: 定义线段交接时的形状
    1. StrokeJoin.miter 默认,当两条线段夹角小于30°时,StrokeJoin.miter将会变成StrokeJoin.bevel

miter 2. StrokeJoin.bevel

bevel

  1. StrokeJoin.round

round

  • strokeMiterLimit: 当strokeJoinStrokeJoin.miter时且stylePaintingStyle.stroke有效,用来设置连接线的长度,一般可用strokeJoin来替换
  • imageFilter: 设置模糊度
    1. ImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0, TileMode tileMode = TileMode.clamp}): sigmaX与sigmaY在0~10之间,数值越大越模糊
    2. ImageFilter.matrix 使用matrix来创建模糊度
    3. ImageFilter.compose 组合两个ImageFilter
  • invertColors: 反转画笔颜色(跟设置的color有关)
  • blendMode: 混合模式,两个形状混合时使用的模式,具体可参考blendMode,默认为BlendMode.srcOver
  • shader: 着色器
  • maskFilter: 模糊蒙版滤镜,比如绘制一些阴影效果或者艺术字等
  • filterQuality: 设置滤镜(如maskFilter或者image)的质量
  • colorFilter: 彩色矩阵滤色器,可以通过设置此属性改变画笔颜色如黑白色

Canvas与绘制无关API

save

save操作会保存此前的所有绘制内容和canvas状态。在调用该函数之后的绘制操作和变换操作,会重新记录。当你调用restore()之后,会把saverestore之间所进行的操作与之前的内容进行合并。 下面看一个例子

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);

我使用一个函数画了三个形状一样的图形,只是它们的位置不一样,然后在第一个图形后面使用了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);

看到区别了吧,最后绘制的图形是没有跟随上面部分旋转的。使用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

getSaveCount

通过此获取当前位置调用save多少次,最开始是1,当调用一个save或者saveLayer+1,遇到restore后-1

总结

本篇文章介绍了我们在使用canvas前的一些需要掌握的知识,后续会介绍canvas的一些绘制能力,如有兴趣,请关注本篇文章原地址,后面会继续更新。