28.Flutter:成爲Canvas繪製大師(四)

目錄傳送門:《Flutter快速上手指南》先導篇html

經過前面 3 篇:git

相信你已經掌握了 Flutter 中繪製基礎圖形的操做,本篇將會講解 Canvas 的變換操做。函數

save()、saveLayer() 和 restore()

在開始瞭解 Canvas 的變換操做時,先看看 Canvas 的 save()saveLayer()restore()post

在進行變換操做時,你常常會須要用到它們。動畫

save()

save() 操做會保存此前的全部繪製內容和 Canvas 狀態。ui

在調用該函數以後的繪製操做和變換操做,會從新記錄。spa

當你調用 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,其 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();
    複製代碼

    🖼 效果:

    前面的圖層繪製了一張圖片,在新圖層中,繪製了一個矩形。

    若是 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();
複製代碼

🖼 效果:

效果仍是比較明顯的 😀。

目錄傳送門:《Flutter快速上手指南》先導篇

如何找到我?

傳送門:CoorChice 的主頁

傳送門:CoorChice 的 Github

相關文章
相關標籤/搜索