28.Flutter:成为Canvas绘制大师(四)

9,446 阅读6分钟

目录传送门:《Flutter快速上手指南》先导篇

通过前面 3 篇:

相信你已经掌握了 Flutter 中绘制基础图形的操作,本篇将会讲解 Canvas 的变换操作。

save()、saveLayer() 和 restore()

在开始了解 Canvas 的变换操作时,先看看 Canvas 的 save()saveLayer()restore()

在进行变换操作时,你经常会需要用到它们。

save()

save() 操作会保存此前的所有绘制内容和 Canvas 状态。

在调用该函数之后的绘制操作和变换操作,会重新记录。

当你调用 restore() 之后,会把 save()restore() 之间所进行的操作与之前的内容进行合并。

⚠️ 注意,save() 并不会创建新的图层,和 saveLayer() 是不同的。

saveLayer()

saveLayer() 在大多数情况下看起来和 save() 的效果是差不多的。

不同的是 saveLayer() 会创建一个新的图层。

saveLayer()restore() 之间的操作,是在新的图层上进行的,虽然最终它们还是会合成到一起。

看看 saveLayer() 的两个参数:

  • rect

    Rect,用于设置新图层的范围区域。

    你的绘制操作只有在这个区域内才会有效,超过这个区域的部分会被忽略。

    🌰 e.g.:

    canvas.saveLayer(Rect.fromCircle(
        center: Offset(size.width / 2, size.height / 2), radius: 100), paint);
    // 用颜色填充整个绘制区域
    canvas.drawPaint(Paint()..color = Colors.blue);
    // 在绘制区域以外绘制一个矩形
    canvas.drawRect(Rect.fromLTWH(0, 0, 100, 100), Paint()..color = Colors.red);
    canvas.restore();
    

    🖼 效果:

    从这个例子中可以看到,新图层的绘制内容被限制在了 rect 范围内。

  • paint

    Paint,其 ColorFiltersBlendMode 配置会在图层合成的时候生效。

    其中,前面的图层为 dst,本图层为 src

    🌰 e.g.:

    canvas.saveLayer(Rect.fromCircle(
        center: Offset(size.width / 2, size.height / 2), radius: 60), Paint()..color = Colors.red);
    canvas.drawPaint(Paint()..color = Colors.amber);
    canvas.restore();
    

    🖼 效果:

    前面的图层绘制了一张图片,在新图层中,绘制了一个矩形。

    如果 Paint 没有设置混合参数,新图层就相当于仅仅是盖在了前面的图层之上。

    ⚠️ 注意,在传入的 Paint 必须设置过 color,否则你设置的 rect 范围限制将会失效!

    如果将 Paint 设置 BlendMode 混合模式,再看看效果。

    canvas.saveLayer(Rect.fromCircle(
        center: Offset(size.width / 2, size.height / 2), radius: 60), 
         Paint()
           ..color=Colors.red
           ..blendMode=BlendMode.exclusion);
    

    🖼 效果:

    可以看到,新的图层和之前的内容的像素进行了混合。

    💡 提示,BlendMode 的支持的所有混合效果,可以参考:BlendMode API

restore()

读到这,相信你对 restore() 也不会陌生了。

在调用 save() 或者 saveLayer() 必须调用 restore() 来合成,否则 Flutter 会抛出异常。

值得注意的是,每一个 save() 或者 saveLayer() 都必须有一个对应的 restore()

🌰 e.g.:

// save-1  
canvas.save();
...
// save-2
canvas.saveLayer(dstRect, paint);
...
// save-3
canvas.saveLayer(dstRect, paint);
...
// restore-3
canvas.restore();
// restore-2
canvas.restore();
// restore-1
canvas.restore(); 

restore() 是从离它最近的 save() 或者 saveLayer() 操作开始合成。

⚠️ 注意,Canvas 的变化操作需要放到 save() 或者 saveLayer()restore() 之间,否则你很难得到想要的效果。

平移画布translate()

translate() 用于将画布相对于原来的位置,平移指定的距离。

下面看个例子 🌰。

先在画布中画一张图:

canvas.drawImage(background, Offset.zero, paint);

🖼 效果:

现在,将画布平移:

canvas.save();
// 平移画布
canvas.translate(100, 100);
canvas.drawImage(background, Offset.zero, paint);
canvas.restore();

🖼 效果:

绘制图片的逻辑不变,但经过平移后,图片的位置发生了变化。

缩放画布scale()

scale() 用于将画布进行缩放。

直接看例子 🌰。

先画一个充满画布的矩形:

canvas.drawRect(Offset.zero & size, Paint()..color=Colors.pinkAccent);

🖼 效果:

现在,将画布进行缩放:

canvas.save();
canvas.scale(0.5);
canvas.drawRect(Offset.zero & size, Paint()..color=Colors.pinkAccent);
canvas.restore();

🖼 效果:

将画布缩小一半后,可以看到原来的矩形也缩小了一半。

旋转画布rotate()

rotate() 用于旋转画布。

看着例子 🌰 来理解它的用法。

先在画布的中心位置画一个矩形:

canvas.drawRect(Rect.fromCircle(
        center: Offset(size.width / 2, size.height / 2), radius: 100), Paint()..color = Colors.amber);

🖼 效果:

现在,旋转45度:

canvas.save();
canvas.rotate(pi/4);
canvas.drawRect(Rect.fromCircle(
    center: Offset(size.width / 2, size.height / 2), radius: 100), Paint()..color = Colors.amber);
canvas.restore();

🖼 效果:

看效果图,会发现,矩形确实是旋转了,但是旋转的有点怪 😐。

这是因为,Canvas 的旋转中心是在画布的左上角,所以得到的结果不是想要的。

如何获得预期的中心旋转效果呢?

你需要移动画布,让绕左上角旋转的画布看起来像中心旋转一样。

那么重点就是,如何确定画布需要移动多少偏移量呢?

首先,看看在旋转过程中,画布的中心位置是如何变化的吧:

💡提示,Canvas 的正向旋转方向为顺时针方向,且 0 弧度在图中 x 轴正方向上。

从图中可以看到,当画布围绕左上角旋转时,画布的中心点始终在以 左上角为圆心画布对角线的一半 为半径的圆上移动。

画布需要移动的偏移量实际上就是 圆上各点(旋转后的画布中心点) 到画布 初始中心点 的距离的一半。

那么这个问题就被转化为了:求圆上两点之间的距离的问题

现在,来解决它吧 🤨!

现在的已知条件只有:画布的尺寸,size

但这就够了。

1.计算画布 初始中心点 的坐标。

求圆上某点的坐标,可以通过以下公式计算:

x = x0 + r * cos(𝒶)
y = y0 + r * sin(𝒶)

因为圆心为画布左上角,即 (0, 0) 点,所以可以简化为:

x = r * cos(𝒶)
y = r * sin(𝒶)

显然,要计算画布 初始中心点 的坐标,先要计算中心点轨迹圆的半径,以及该点所在弧度。

根据 勾股定理 很容易计算出中心点轨迹圆的半径:

double r = sqrt(pow(size.width, 2) + pow(size.height, 2));

根据 反正弦函数,可以计算出 初始中心点 的弧度:

double startAngle = atan(size.height / size.width);

现在,就可以很轻松的求解出画布 初始中心点 的坐标:

double x0 = r * cos(startAngle);
double y0 = r * sin(startAngle);
Point p0 = Point(x0, y0);

2.计算旋转后的画布的中心点坐标

回顾一下上面的图,当画布旋转 𝒶 弧度后,其中心点所在的弧度为 𝒶 + 画布初始中心点的弧度,则:

double realAngle = xAngle + startAngle;

获得了中心点的角度,那计算它的坐标也就轻而易举了:

Point px = Point(r * cos(realAngle), r * sin(realAngle));  

3.平移画布

现在,我们获得了画布 初始中心点 的坐标和画布旋转后的中心点坐标,就可以知道画布应该平移多少了:

canvas.translate((p0.x - px.x)/2, (p0.y - px.y)/2);

4.完整代码

把上面的代码,带入刚刚的旋转操作中:

canvas.save();
// 计算画布中心轨迹圆半径
double r = sqrt(pow(size.width, 2) + pow(size.height, 2));
// 计算画布中心点初始弧度
double startAngle = atan(size.height / size.width);
// 计算画布初始中心点坐标
Point p0 = Point(r * cos(startAngle), r * sin(startAngle));
// 需要旋转的弧度
double xAngle = pi / 4;
// 计算旋转后的画布中心点坐标
Point px = Point(
    r * cos(xAngle + startAngle), r * sin(xAngle + startAngle));
// 先平移画布
canvas.translate((p0.x - px.x) / 2, (p0.y - px.y) / 2);
// 后旋转
canvas.rotate(xAngle);
canvas.drawRect(Rect.fromCircle(
    center: Offset(size.width / 2, size.height / 2), radius: 100), Paint()
  ..color = Colors.amber);
canvas.restore();

🖼 效果:

💡提示,rotate() 是以弧度制进行的。

斜切画布skew()

skew() 用于斜切画布,它有两个参数,第一个表示水平方向的斜切,第二个表示垂直方向的斜切,斜切值是正弦函数 tan值。

比如,斜切 45 度,即 tan(pi/4) = 1

看例子 🌰。

先在画布中心位置画一张图片:

canvas.drawImageRect(background, Offset.zero & imgSize,
        Alignment.center.inscribe(imgSize, Offset.zero & size), paint);

🖼 效果:

进行斜切操作:

canvas.save();
canvas.skew(0.2, 0);
canvas.drawImageRect(background, Offset.zero & imgSize,
    Alignment.center.inscribe(imgSize, Offset.zero & size), paint);
canvas.restore();

🖼 效果:

效果还是比较明显的 😀。

目录传送门:《Flutter快速上手指南》先导篇

如何找到我?

传送门:CoorChice 的主页

传送门:CoorChice 的 Github