经过前面 3 篇:git
相信你已经掌握了 Flutter 中绘制基础图形的操做,本篇将会讲解 Canvas 的变换操做。函数
在开始了解 Canvas 的变换操做时,先看看 Canvas 的 save()
、saveLayer()
和 restore()
。post
在进行变换操做时,你常常会须要用到它们。动画
save()
操做会保存此前的全部绘制内容和 Canvas 状态。ui
在调用该函数以后的绘制操做和变换操做,会从新记录。spa
当你调用 restore()
以后,会把 save()
到 restore()
之间所进行的操做与以前的内容进行合并。
⚠️ 注意,
save()
并不会建立新的图层,和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()
也不会陌生了。
在调用 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()
用于将画布相对于原来的位置,平移指定的距离。
下面看个例子 🌰。
先在画布中画一张图:
canvas.drawImage(background, Offset.zero, paint);
复制代码
🖼 效果:
如今,将画布平移:
canvas.save();
// 平移画布
canvas.translate(100, 100);
canvas.drawImage(background, Offset.zero, paint);
canvas.restore();
复制代码
🖼 效果:
绘制图片的逻辑不变,但通过平移后,图片的位置发生了变化。
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()
用于旋转画布。
看着例子 🌰 来理解它的用法。
先在画布的中心位置画一个矩形:
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。
但这就够了。
求圆上某点的坐标,能够经过如下公式计算:
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);
复制代码
回顾一下上面的图,当画布旋转 𝒶
弧度后,其中心点所在的弧度为 𝒶 + 画布初始中心点的弧度
,则:
double realAngle = xAngle + startAngle;
复制代码
得到了中心点的角度,那计算它的坐标也就垂手可得了:
Point px = Point(r * cos(realAngle), r * sin(realAngle));
复制代码
如今,咱们得到了画布 初始中心点 的坐标和画布旋转后的中心点坐标,就能够知道画布应该平移多少了:
canvas.translate((p0.x - px.x)/2, (p0.y - px.y)/2);
复制代码
把上面的代码,带入刚刚的旋转操做中:
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()
用于斜切画布,它有两个参数,第一个表示水平方向的斜切,第二个表示垂直方向的斜切,斜切值是正弦函数 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();
复制代码
🖼 效果:
效果仍是比较明显的 😀。