這期是 HenCoder 自定義繪製的第 1-4 期:Canvas 對繪製的輔助——範圍裁切和幾何變換。
之前的內容在這裡:
HenCoder Android 開發進階 自定義 View 1-1 繪製基礎
HenCoder Android 開發進階 自定義 View 1-2 Paint 詳解
HenCoder Android 開發進階 自定義 View 1-3 文字的繪製
如果你沒聽說過 HenCoder,可以先看看這個:
HenCoder:給高階 Android 工程師的進階手冊
簡介
一圖勝千言,一視訊勝千圖,走你:
如果你是在手機上看的,可以點這裡去 B 站看原視訊。
1 範圍裁切
範圍裁切有兩個方法: clipRect()
和 clipPath()
。裁切方法之後的繪製程式碼,都會被限制在裁切範圍內。
1.1 clipRect()
使用很簡單,直接應用:
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(bitmap, x, y, paint);複製程式碼
記得要加上 Canvas.save()
和 Canvas.restore()
來及時恢復繪製範圍,所以完整程式碼是這樣的:
canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();複製程式碼
1.2 clipPath()
其實和 clipRect() 用法完全一樣,只是把引數換成了 Path
,所以能裁切的形狀更多一些:
canvas.save();
canvas.clipPath(path1);
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
canvas.save();
canvas.clipPath(path2);
canvas.drawBitmap(bitmap, point2.x, point2.y, paint);
canvas.restore();複製程式碼
2 幾何變換
幾何變換的使用大概分為三類:
- 使用
Canvas
來做常見的二維變換; - 使用
Matrix
來做常見和不常見的二維變換; - 使用
Camera
來做三維變換。
2.1 使用 Canvas 來做常見的二維變換:
2.1.1 Canvas.translate(float dx, float dy) 平移
引數裡的 dx
和 dy
表示橫向和縱向的位移。
canvas.save();
canvas.translate(200, 0);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();複製程式碼
好吧這個從截圖並不能看出什麼,那你就用心去感受吧
2.1.2 Canvas.rotate(float degrees, float px, float py) 旋轉
引數裡的 degrees
是旋轉角度,單位是度(也就是一週有 360° 的那個單位),方向是順時針為正向; px
和 py
是軸心的位置。
canvas.save();
canvas.rotate(45, centerX, centerY);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();複製程式碼
2.1.3 Canvas.scale(float sx, float sy, float px, float py) 放縮
引數裡的 sx
sy
是橫向和縱向的放縮倍數; px
py
是放縮的軸心。
canvas.save();
canvas.scale(1.3f, 1.3f, x + bitmapWidth / 2, y + bitmapHeight / 2);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();複製程式碼
2.1.4 skew(float sx, float sy) 錯切
引數裡的 sx
和 sy
是 x 方向和 y 方向的錯切係數。
canvas.save();
canvas.skew(0, 0.5f);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();複製程式碼
2.2 使用 Matrix 來做變換
2.2.1 使用 Matrix 來做常見變換
Matrix
做常見變換的方式:
- 建立
Matrix
物件; - 呼叫
Matrix
的pre/postTranslate/Rotate/Scale/Skew()
方法來設定幾何變換; - 使用
Canvas.setMatrix(matrix)
或Canvas.concat(matrix)
來把幾何變換應用到Canvas
。
Matrix matrix = new Matrix();
...
matrix.reset();
matrix.postTranslate();
matrix.postRotate();
canvas.save();
canvas.concat(matrix);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();複製程式碼
效果就不放圖了,和 Canvas
是一樣的。
把 Matrix
應用到 Canvas
有兩個方法: Canvas.setMatrix(matrix)
和 Canvas.concat(matrix)
。
Canvas.setMatrix(matrix)
:用Matrix
直接替換Canvas
當前的變換矩陣,即拋棄Canvas
當前的變換,改用Matrix
的變換(注:根據我在一些評論以及微信公眾號中收到的反饋,不同的手機系統中 setMatrix(matrix) 的行為可能不一致,所以還是儘量用 concat(matrix) 吧);Canvas.concat(matrix)
:用Canvas
當前的變換矩陣和Matrix
相乘,即基於Canvas
當前的變換,疊加上Matrix
中的變換。
2.2.2 使用 Matrix 來做自定義變換
Matrix
的自定義變換使用的是 setPolyToPoly()
方法。
2.2.2.1 Matrix.setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount) 用點對點對映的方式設定變換
poly
就是「多」的意思。setPolyToPoly()
的作用是通過多點的對映的方式來直接設定變換。「多點對映」的意思就是把指定的點移動到給出的位置,從而發生形變。例如:(0, 0) -> (100, 100) 表示把 (0, 0) 位置的畫素移動到 (100, 100) 的位置,這個是單點的對映,單點對映可以實現平移。而多點的對映,就可以讓繪製內容任意地扭曲。
Matrix matrix = new Matrix();
float pointsSrc = {left, top, right, top, left, bottom, right, bottom};
float pointsDst = {left - 10, top + 50, right + 120, top - 90, left + 20, bottom + 30, right + 20, bottom + 60};
...
matrix.reset();
matrix.setPolyToPoly(pointsSrc, 0, pointsDst, 0, 4);
canvas.save();
canvas.concat(matrix);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();複製程式碼
引數裡,src
和 dst
是源點集合目標點集;srcIndex
和 dstIndex
是第一個點的偏移;pointCount
是採集的點的個數(個數不能大於 4,因為大於 4 個點就無法計算變換了)。
2.3 使用 Camera 來做三維變換
Camera
的三維變換有三類:旋轉、平移、移動相機。
2.3.1 Camera.rotate*() 三維旋轉
Camera.rotate*()
一共有四個方法: rotateX(deg)
rotateY(deg)
rotateZ(deg)
rotate(x, y, z)
。這四個方法的區別不用我說了吧?
canvas.save();
camera.rotateX(30); // 旋轉 Camera 的三維空間
camera.applyToCanvas(canvas); // 把旋轉投影到 Canvas
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();複製程式碼
另外,Camera
和 Canvas
一樣也需要儲存和恢復狀態才能正常繪製,不然在介面重新整理之後繪製就會出現問題。所以上面這張圖完整的程式碼應該是這樣的:
canvas.save();
camera.save(); // 儲存 Camera 的狀態
camera.rotateX(30); // 旋轉 Camera 的三維空間
camera.applyToCanvas(canvas); // 把旋轉投影到 Canvas
camera.restore(); // 恢復 Camera 的狀態
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();複製程式碼
如果你需要圖形左右對稱,需要配合上 Canvas.translate()
,在三維旋轉之前把繪製內容的中心點移動到原點,即旋轉的軸心,然後在三維旋轉後再把投影移動回來:
canvas.save();
camera.save(); // 儲存 Camera 的狀態
camera.rotateX(30); // 旋轉 Camera 的三維空間
canvas.translate(centerX, centerY); // 旋轉之後把投影移動回來
camera.applyToCanvas(canvas); // 把旋轉投影到 Canvas
canvas.translate(-centerX, -centerY); // 旋轉之前把繪製內容移動到軸心(原點)
camera.restore(); // 恢復 Camera 的狀態
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();複製程式碼
Canvas
的幾何變換順序是反的,所以要把移動到中心的程式碼寫在下面,把從中心移動回來的程式碼寫在上面。
2.3.2 Camera.translate(float x, float y, float z) 移動
它的使用方式和 Camera.rotate*()
相同,而且我在專案中沒有用過它,所以就不貼程式碼和效果圖了。
2.3.3 Camera.setLocation(x, y, z) 設定虛擬相機的位置
注意!這個方法有點奇葩,它的引數的單位不是畫素,而是 inch,英寸。
我 TM 的真沒逗你,我也沒有胡說八道,它的單位就。是。英。寸。
這種設計源自 Android 底層的影像引擎 Skia 。在 Skia 中,Camera 的位置單位是英寸,英寸和畫素的換算單位在 Skia 中被寫死為了 72 畫素,而 Android 中把這個換算單位照搬了過來。是的,它。寫。死。了。
吐槽到此為止,俗話說看透不說透,還是好朋友。
在 Camera
中,相機的預設位置是 (0, 0, -8)(英寸)。8 x 72 = 576,所以它的預設位置是 (0, 0, -576)(畫素)。
如果繪製的內容過大,當它翻轉起來的時候,就有可能出現影像投影過大的「糊臉」效果。而且由於換算單位被寫死成了 72 畫素,而不是和裝置 dpi 相關的,所以在畫素越大的手機上,這種「糊臉」效果會越明顯。
而使用 setLocation()
方法來把相機往後移動,就可以修復這種問題。
camera.setLocation(0, 0, newZ);複製程式碼
Camera.setLocation(x, y, z)
的 x
和 y
引數一般不會改變,直接填 0 就好。
好了,上面這些就是本期的內容:範圍裁切和幾何變換。
練習專案
為了避免轉頭就忘,強烈建議你趁熱打鐵,做一下這個練習專案:HenCoderPracticeDraw4
下期預告
到這期為止,所有繪製的「術」就講完了,下期講的是「道」:繪製順序。
特別感謝
這次由於某些原因,沒有依慣例邀請內測讀者,但我需要特別感謝幾個人:
- 特別感謝 GcsSloop 和 LeeThree 在我疲憊不堪的時候和我探討問題、幫我做實驗和尋找答案。
- 特別感謝 脈脈不得語 在我忙得不可開交的時候幫我解決我的各種大小問題,以及幫我聯絡廣告贊助商(別找了,這期沒上廣告,以後再上)。
覺得贊?
如果你看完覺得有收穫,把文章轉發到你的微博、微信群、朋友圈、公眾號,讓其他需要的人也看到吧。