通過前面 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();
複製程式碼
? 效果:
效果還是比較明顯的 ?。