通过前面 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,其
ColorFilters
和BlendMode
配置会在图层合成的时候生效。其中,前面的图层为 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();
🖼 效果:
效果还是比较明显的 😀。