HenCoder Android 開發進階:自定義 View 1-4 Canvas 對繪製的輔助

扔物線發表於2017-08-04

這期是 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 幾何變換

幾何變換的使用大概分為三類:

  1. 使用 Canvas 來做常見的二維變換;
  2. 使用 Matrix 來做常見和不常見的二維變換;
  3. 使用 Camera 來做三維變換。

2.1 使用 Canvas 來做常見的二維變換:

2.1.1 Canvas.translate(float dx, float dy) 平移

引數裡的 dxdy 表示橫向和縱向的位移。

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° 的那個單位),方向是順時針為正向; pxpy 是軸心的位置。

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) 錯切

引數裡的 sxsy 是 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 做常見變換的方式:

  1. 建立 Matrix 物件;
  2. 呼叫 Matrixpre/postTranslate/Rotate/Scale/Skew() 方法來設定幾何變換;
  3. 使用 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)

  1. Canvas.setMatrix(matrix):用 Matrix 直接替換 Canvas 當前的變換矩陣,即拋棄 Canvas 當前的變換,改用 Matrix 的變換(注:根據我在一些評論以及微信公眾號中收到的反饋,不同的手機系統中 setMatrix(matrix) 的行為可能不一致,所以還是儘量用 concat(matrix) 吧);
  2. 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();複製程式碼

引數裡,srcdst 是源點集合目標點集;srcIndexdstIndex 是第一個點的偏移;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();複製程式碼

另外,CameraCanvas 一樣也需要儲存和恢復狀態才能正常繪製,不然在介面重新整理之後繪製就會出現問題。所以上面這張圖完整的程式碼應該是這樣的:

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)xy 引數一般不會改變,直接填 0 就好。

好了,上面這些就是本期的內容:範圍裁切和幾何變換。

練習專案

為了避免轉頭就忘,強烈建議你趁熱打鐵,做一下這個練習專案:HenCoderPracticeDraw4

下期預告

到這期為止,所有繪製的「術」就講完了,下期講的是「道」:繪製順序。

特別感謝

這次由於某些原因,沒有依慣例邀請內測讀者,但我需要特別感謝幾個人:

  • 特別感謝 GcsSloopLeeThree 在我疲憊不堪的時候和我探討問題、幫我做實驗和尋找答案。
  • 特別感謝 脈脈不得語 在我忙得不可開交的時候幫我解決我的各種大小問題,以及幫我聯絡廣告贊助商(別找了,這期沒上廣告,以後再上)。

覺得贊?

如果你看完覺得有收穫,把文章轉發到你的微博、微信群、朋友圈、公眾號,讓其他需要的人也看到吧。

相關文章