28.Flutter:成為Canvas繪制大師(四)

目錄傳送門:《Flutter快速上手指南》先導(dǎo)篇

通過前面 3 篇:

相信你已經(jīng)掌握了 Flutter 中繪制基礎(chǔ)圖形的操作唠雕,本篇將會(huì)講解 Canvas 的變換操作想帅。

save()鼻弧、saveLayer() 和 restore()

在開始了解 Canvas 的變換操作時(shí),先看看 Canvas 的 save()saveLayer()restore()耍攘。

在進(jìn)行變換操作時(shí)武通,你經(jīng)常會(huì)需要用到它們。

save()

save() 操作會(huì)保存此前的所有繪制內(nèi)容和 Canvas 狀態(tài)计螺。

在調(diào)用該函數(shù)之后的繪制操作和變換操作夯尽,會(huì)重新記錄。

當(dāng)你調(diào)用 restore() 之后登馒,會(huì)把 save()restore() 之間所進(jìn)行的操作與之前的內(nèi)容進(jìn)行合并匙握。

?? 注意,save() 并不會(huì)創(chuàng)建新的圖層陈轿,和 saveLayer() 是不同的圈纺。

saveLayer()

saveLayer() 在大多數(shù)情況下看起來和 save() 的效果是差不多的。

不同的是 saveLayer() 會(huì)創(chuàng)建一個(gè)新的圖層麦射。

saveLayer()restore() 之間的操作蛾娶,是在新的圖層上進(jìn)行的,雖然最終它們還是會(huì)合成到一起潜秋。

看看 saveLayer() 的兩個(gè)參數(shù):

  • rect

    Rect蛔琅,用于設(shè)置新圖層的范圍區(qū)域。

    你的繪制操作只有在這個(gè)區(qū)域內(nèi)才會(huì)有效峻呛,超過這個(gè)區(qū)域的部分會(huì)被忽略罗售。

    ?? e.g.:

    canvas.saveLayer(Rect.fromCircle(
        center: Offset(size.width / 2, size.height / 2), radius: 100), paint);
    // 用顏色填充整個(gè)繪制區(qū)域
    canvas.drawPaint(Paint()..color = Colors.blue);
    // 在繪制區(qū)域以外繪制一個(gè)矩形
    canvas.drawRect(Rect.fromLTWH(0, 0, 100, 100), Paint()..color = Colors.red);
    canvas.restore();
    

    ?? 效果:

    image

    從這個(gè)例子中可以看到,新圖層的繪制內(nèi)容被限制在了 rect 范圍內(nèi)杀饵。

  • paint

    Paint莽囤,其 ColorFiltersBlendMode 配置會(huì)在圖層合成的時(shí)候生效。

    其中切距,前面的圖層為 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

    前面的圖層繪制了一張圖片谜悟,在新圖層中话肖,繪制了一個(gè)矩形。

    如果 Paint 沒有設(shè)置混合參數(shù)葡幸,新圖層就相當(dāng)于僅僅是蓋在了前面的圖層之上最筒。

    ?? 注意,在傳入的 Paint 必須設(shè)置過 color蔚叨,否則你設(shè)置的 rect 范圍限制將會(huì)失效床蜘!

    如果將 Paint 設(shè)置 BlendMode 混合模式辙培,再看看效果。

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

    ?? 效果:

    image

    可以看到邢锯,新的圖層和之前的內(nèi)容的像素進(jìn)行了混合扬蕊。

    ?? 提示,BlendMode 的支持的所有混合效果丹擎,可以參考:BlendMode API尾抑。

restore()

讀到這,相信你對(duì) restore() 也不會(huì)陌生了蒂培。

在調(diào)用 save() 或者 saveLayer() 必須調(diào)用 restore() 來合成再愈,否則 Flutter 會(huì)拋出異常。

值得注意的是护戳,每一個(gè) save() 或者 saveLayer() 都必須有一個(gè)對(duì)應(yīng)的 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() 用于將畫布相對(duì)于原來的位置肺樟,平移指定的距離。

下面看個(gè)例子 ??逻淌。

先在畫布中畫一張圖:

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

?? 效果:

image

現(xiàn)在么伯,將畫布平移:

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

?? 效果:

image

繪制圖片的邏輯不變,但經(jīng)過平移后卡儒,圖片的位置發(fā)生了變化田柔。

縮放畫布scale()

scale() 用于將畫布進(jìn)行縮放。

直接看例子 ??骨望。

先畫一個(gè)充滿畫布的矩形:

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

?? 效果:

image

現(xiàn)在硬爆,將畫布進(jìn)行縮放:

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

?? 效果:

image

將畫布縮小一半后,可以看到原來的矩形也縮小了一半擎鸠。

旋轉(zhuǎn)畫布rotate()

rotate() 用于旋轉(zhuǎn)畫布缀磕。

看著例子 ?? 來理解它的用法。

先在畫布的中心位置畫一個(gè)矩形:

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

?? 效果:

image

現(xiàn)在劣光,旋轉(zhuǎn)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

看效果圖袜蚕,會(huì)發(fā)現(xiàn),矩形確實(shí)是旋轉(zhuǎn)了绢涡,但是旋轉(zhuǎn)的有點(diǎn)怪 ??牲剃。

這是因?yàn)椋珻anvas 的旋轉(zhuǎn)中心是在畫布的左上角雄可,所以得到的結(jié)果不是想要的凿傅。

如何獲得預(yù)期的中心旋轉(zhuǎn)效果呢缠犀?

你需要移動(dòng)畫布,讓繞左上角旋轉(zhuǎn)的畫布看起來像中心旋轉(zhuǎn)一樣聪舒。

那么重點(diǎn)就是夭坪,如何確定畫布需要移動(dòng)多少偏移量呢?

首先过椎,看看在旋轉(zhuǎn)過程中室梅,畫布的中心位置是如何變化的吧:

image

??提示,Canvas 的正向旋轉(zhuǎn)方向?yàn)轫槙r(shí)針方向疚宇,且 0 弧度在圖中 x 軸正方向上亡鼠。

從圖中可以看到,當(dāng)畫布圍繞左上角旋轉(zhuǎn)時(shí)敷待,畫布的中心點(diǎn)始終在以 左上角為圓心间涵,畫布對(duì)角線的一半 為半徑的圓上移動(dòng)。

畫布需要移動(dòng)的偏移量實(shí)際上就是 圓上各點(diǎn)(旋轉(zhuǎn)后的畫布中心點(diǎn)) 到畫布 初始中心點(diǎn) 的距離的一半榜揖。

那么這個(gè)問題就被轉(zhuǎn)化為了:求圓上兩點(diǎn)之間的距離的問題勾哩。

現(xiàn)在,來解決它吧 ??举哟!

現(xiàn)在的已知條件只有:畫布的尺寸思劳,size

但這就夠了妨猩。

1.計(jì)算畫布 初始中心點(diǎn) 的坐標(biāo)潜叛。

求圓上某點(diǎn)的坐標(biāo),可以通過以下公式計(jì)算:

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

因?yàn)閳A心為畫布左上角壶硅,即 (0, 0) 點(diǎn)威兜,所以可以簡(jiǎn)化為:

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

顯然,要計(jì)算畫布 初始中心點(diǎn) 的坐標(biāo)庐椒,先要計(jì)算中心點(diǎn)軌跡圓的半徑椒舵,以及該點(diǎn)所在弧度。

根據(jù) 勾股定理 很容易計(jì)算出中心點(diǎn)軌跡圓的半徑:

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

根據(jù) 反正弦函數(shù)约谈,可以計(jì)算出 初始中心點(diǎn) 的弧度:

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

現(xiàn)在笔宿,就可以很輕松的求解出畫布 初始中心點(diǎn) 的坐標(biāo):

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

2.計(jì)算旋轉(zhuǎn)后的畫布的中心點(diǎn)坐標(biāo)

回顧一下上面的圖,當(dāng)畫布旋轉(zhuǎn) ?? 弧度后窗宇,其中心點(diǎn)所在的弧度為 ?? + 畫布初始中心點(diǎn)的弧度措伐,則:

double realAngle = xAngle + startAngle;

獲得了中心點(diǎn)的角度,那計(jì)算它的坐標(biāo)也就輕而易舉了:

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

3.平移畫布

現(xiàn)在军俊,我們獲得了畫布 初始中心點(diǎn) 的坐標(biāo)和畫布旋轉(zhuǎn)后的中心點(diǎn)坐標(biāo)侥加,就可以知道畫布應(yīng)該平移多少了:

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

4.完整代碼

把上面的代碼,帶入剛剛的旋轉(zhuǎn)操作中:

canvas.save();
// 計(jì)算畫布中心軌跡圓半徑
double r = sqrt(pow(size.width, 2) + pow(size.height, 2));
// 計(jì)算畫布中心點(diǎn)初始弧度
double startAngle = atan(size.height / size.width);
// 計(jì)算畫布初始中心點(diǎn)坐標(biāo)
Point p0 = Point(r * cos(startAngle), r * sin(startAngle));
// 需要旋轉(zhuǎn)的弧度
double xAngle = pi / 4;
// 計(jì)算旋轉(zhuǎn)后的畫布中心點(diǎn)坐標(biāo)
Point px = Point(
    r * cos(xAngle + startAngle), r * sin(xAngle + startAngle));
// 先平移畫布
canvas.translate((p0.x - px.x) / 2, (p0.y - px.y) / 2);
// 后旋轉(zhuǎn)
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() 是以弧度制進(jìn)行的担败。

斜切畫布skew()

skew() 用于斜切畫布昔穴,它有兩個(gè)參數(shù),第一個(gè)表示水平方向的斜切提前,第二個(gè)表示垂直方向的斜切吗货,斜切值是正弦函數(shù) tan 值。

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

看例子 ??拓哺。

先在畫布中心位置畫一張圖片:

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

?? 效果:

image

進(jìn)行斜切操作:

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快速上手指南》先導(dǎo)篇

如何找到我?

傳送門:CoorChice 的主頁

傳送門:CoorChice 的 Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末士鸥,一起剝皮案震驚了整個(gè)濱河市闲孤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烤礁,老刑警劉巖讼积,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異脚仔,居然都是意外死亡勤众,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門玻侥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來决摧,“玉大人亿蒸,你說我怎么就攤上這事凑兰。” “怎么了边锁?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵姑食,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我茅坛,道長(zhǎng)音半,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任贡蓖,我火速辦了婚禮曹鸠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斥铺。我一直安慰自己彻桃,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布晾蜘。 她就那樣靜靜地躺著邻眷,像睡著了一般眠屎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肆饶,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天改衩,我揣著相機(jī)與錄音,去河邊找鬼驯镊。 笑死葫督,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的板惑。 我是一名探鬼主播候衍,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼洒放!你這毒婦竟也來了蛉鹿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤往湿,失蹤者是張志新(化名)和其女友劉穎妖异,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體领追,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡他膳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绒窑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棕孙。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖些膨,靈堂內(nèi)的尸體忽然破棺而出蟀俊,到底是詐尸還是另有隱情,我是刑警寧澤订雾,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布肢预,位于F島的核電站,受9級(jí)特大地震影響洼哎,放射性物質(zhì)發(fā)生泄漏烫映。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一噩峦、第九天 我趴在偏房一處隱蔽的房頂上張望锭沟。 院中可真熱鬧,春花似錦识补、人聲如沸族淮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞧筛。三九已至厉熟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間较幌,已是汗流浹背揍瑟。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留乍炉,地道東北人绢片。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像岛琼,于是被迫代替她去往敵國和親底循。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348