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

目录传送门:《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();
    

    🖼 效果:

    image

    从这个例子中可以看到,新图层的绘制内容被限制在了 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();
    

    🖼 效果:

    image

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

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

    🖼 效果:

    image

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

    💡 提示,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);

🖼 效果:

image

现在,将画布平移:

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

🖼 效果:

image

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

缩放画布scale()

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

直接看例子 🌰。

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

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

🖼 效果:

image

现在,将画布进行缩放:

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

🖼 效果:

image

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

旋转画布rotate()

rotate() 用于旋转画布。

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

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

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

🖼 效果:

image

现在,旋转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();

🖼 效果:

image

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

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

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

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

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

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

image

💡提示,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();

🖼 效果:

image

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

斜切画布skew()

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

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

看例子 🌰。

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

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

🖼 效果:

image

进行斜切操作:

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

🖼 效果:

image

效果还是比较明显的 😀。

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

如何找到我?

传送门:CoorChice 的主页

传送门:CoorChice 的 Github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350