1. 程式人生 > >安卓自定義View進階-Matrix原理

安卓自定義View進階-Matrix原理

本文內容偏向理論,和 畫布操作 有重疊的部分,本文會讓你更加深入的瞭解其中的原理。

本篇的主角Matrix,是一個一直在後臺默默工作的勞動模範,雖然我們所有看到View背後都有著Matrix的功勞,但我們卻很少見到它,本篇我們就看看它是何方神聖吧。

由於Google已經對這一部分已經做了很好的封裝,所以跳過本部分對實際開發影響並不會太大,不想深究的粗略瀏覽即可,下一篇中將會詳細講解Matrix的具體用法和技巧。

⚠️ 警告:測試本文章示例之前請關閉硬體加速。

Matrix簡介

Matrix是一個矩陣,主要功能是座標對映,數值轉換。

它看起來大概是下面這樣:

Matrix作用就是座標對映,那麼為什麼需要Matrix呢? 舉一個簡單的例子:

我的的手機螢幕作為物理裝置,其物理座標系是從左上角開始的,但我們在開發的時候通常不會使用這一座標系,而是使用內容區的座標系。

以下圖為例,我們的內容區和螢幕座標系還相差一個通知欄加一個標題欄的距離,所以兩者是不重合的,我們在內容區的座標系中的內容最終繪製的時候肯定要轉換為實際的物理座標系來繪製,Matrix在此處的作用就是轉換這些數值。

假設通知欄高度為20畫素,導航欄高度為40畫素,那麼我們在內容區的(0,0)位置繪製一個點,最終就要轉化為在實際座標系中的(0,60)位置繪製一個點。

以上是僅作為一個簡單的示例,實際上不論2D還是3D,我們要將圖形顯示在螢幕上,都離不開Matrix,所以說Matrix是一個在背後辛勤工作的勞模。

Matrix特點

  • 作用範圍更廣,Matrix在View,圖片,動畫效果等各個方面均有運用,相比與之前講解等畫布操作應用範圍更廣。

  • 更加靈活,畫布操作是對Matrix的封裝,Matrix作為更接近底層的東西,必然要比畫布操作更加靈活。

  • 封裝很好,Matrix本身對各個方法就做了很好的封裝,讓開發者可以很方便的操作Matrix。

  • 難以深入理解,很難理解中各個數值的意義,以及操作規律,如果不瞭解矩陣,也很難理解前乘,後乘。

常見誤解

1.認為Matrix最下面的一行的三個引數(MPERSP_0、MPERSP_1、MPERSP_2)沒有什麼太大的作用,在這裡只是為了湊數。

實際上最後一行引數在3D變換中有著至關重要的作用,這一點會在後面中Camera一文中詳細介紹。

2.最後一個引數MPERSP_2被解釋為scale

的確,更改MPERSP_2的值能夠達到類似縮放的效果,但這是因為齊次座標的緣故,並非這個引數的實際功能。

Matrix基本原理

Matrix 是一個矩陣,最根本的作用就是座標轉換,下面我們就看看幾種常見變換的原理:

我們所用到的變換均屬於仿射變換,仿射變換是 線性變換(縮放,旋轉,錯切) 和 平移變換(平移) 的複合,由於這些概念對於我們作用並不大,此處不過多介紹,有興趣可自行了解。

基本變換有4種: 平移(translate)、縮放(scale)、旋轉(rotate) 和 錯切(skew)。

下面我們看一下四種變換都是由哪些引數控制的。

從上圖可以看到最後三個引數是控制透視的,這三個引數主要在3D效果中運用,通常為(0, 0, 1),不在本篇討論範圍內,暫不過多敘述,會在之後對文章中詳述其作用。

由於我們以下大部分的計算都是基於矩陣乘法規則,如果你已經把線性代數還給了老師,請參考一下這裡: 維基百科-矩陣乘法

1.縮放(Scale)

用矩陣表示:

你可能注意到了,我們座標多了一個1,這是使用了齊次座標系的緣故,在數學中我們的點和向量都是這樣表示的(x, y),兩者看起來一樣,計算機無法區分,為此讓計算機也可以區分它們,增加了一個標誌位,增加之後看起來是這樣:

(x, y, 1) - 點
(x, y, 0) - 向量

另外,齊次座標具有等比的性質,(2,3,1)、(4,6,2)…(2N,3N,N)表示的均是(2,3)這一個點。(將MPERSP_2解釋為scale這一誤解就源於此)。

圖例:

2.錯切(Skew)

錯切存在兩種特殊錯切,水平錯切(平行X軸)和垂直錯切(平行Y軸)。

水平錯切

用矩陣表示:

圖例:

垂直錯切

用矩陣表示:

圖例:

複合錯切

水平錯切和垂直錯切的複合。

用矩陣表示:

圖例:

3.旋轉(Rotate)

假定一個點 A(x0, y0) ,距離原點距離為 r, 與水平軸夾角為 α 度, 繞原點旋轉 θ 度, 旋轉後為點 B(x, y) 如下:

用矩陣表示:

圖例:

4.平移(Translate)

此處也是使用齊次座標的優點體現之一,實際上前面的三個操作使用 2x2 的矩陣也能滿足需求,但是使用 2x2 的矩陣,無法將平移操作加入其中,而將座標擴充套件為齊次座標後,將矩陣擴充套件為 3x3 就可以將演算法統一,四種演算法均可以使用矩陣乘法完成。

用矩陣表示:

圖例:

Matrix複合原理

其實Matrix的多種複合操作都是使用矩陣乘法實現的,從原理上理解很簡單,但是,使用矩陣乘法也有其弱點,後面的操作可能會影響到前面到操作,所以在構造Matrix時順序很重要。

我們常用的四大變換操作,每一種操作在Matrix均有三類,前乘(pre),後乘(post)和設定(set),可以參見文末對Matrix方法表,由於矩陣乘法不滿足交換律,所以前乘(pre),後乘(post)和設定(set)的區別還是很大的。

前乘(pre)

前乘相當於矩陣的右乘:

這表示一個矩陣與一個特殊矩陣前乘後構造出結果矩陣。

後乘(post)

前乘相當於矩陣的左乘:

這表示一個矩陣與一個特殊矩陣後乘後構造出結果矩陣。

設定(set)

設定使用的不是矩陣乘法,而是直接覆蓋掉原來的數值,所以,使用設定可能會導致之前的操作失效

組合

關於 Matrix 的文章終有一個問題,就是 pre 和 post 這一部分的理論非常彆扭,國內大多數文章都是這樣的,看起來貌似是對的但很難理解,部分內容違背直覺。

我由於也受到了這些文章的影響,自然而然的繼承了這一理論,直到在評論區有一位小夥伴提出了一個問題,才讓我重新審視了這一部分的內容,並進行了一定反思。

經過良久的思考之後,我決定拋棄國內大部分文章的那套理論和結論,只用嚴謹的數學邏輯和程式邏輯來闡述這一部分的理論,也許仍有疏漏,如有發現請指正。

首先澄清兩個錯誤結論,記住,是錯誤結論,錯誤結論,錯誤結論。

錯誤結論一:pre 是順序執行,post 是逆序執行。

這個結論很具有迷惑性,因為這個結論並非是完全錯誤的,你很容易就能證明這個結論,例如下面這樣:

// 第一段 pre  順序執行,先平移(T)後旋轉(R)
Matrix matrix = new Matrix();
matrix.preTranslate(pivotX,pivotY);
matrix.preRotate(angle);
Log.e("Matrix", matrix.toShortString());

// 第二段 post 逆序執行,先平移(T)後旋轉(R)
Matrix matrix = new Matrix();
matrix.postRotate(angle);
matrix.postTranslate(pivotX,pivotY)
Log.e("Matrix", matrix.toShortString());

這兩段程式碼最終結果是等價的,於是輕鬆證得這個結論的正確性,但事實真是這樣麼?

首先,從數學角度分析,pre 和 post 就是右乘或者左乘的區別,其次,它們不可能實際影響運算順序(程式執行順序)。以上這兩段程式碼等價也僅僅是因為最終化簡公式一樣而已。

設原始矩陣為 M,平移為 T ,旋轉為 R ,單位矩陣為 I ,最終結果為 M’

  • 矩陣乘法不滿足交換律,即 A*B ≠ B*A
  • 矩陣乘法滿足結合律,即 (A*B)*C = A*(B*C)
  • 矩陣與單位矩陣相乘結果不變,即 A * I = A
由於上面例子中原始矩陣(M)是一個單位矩陣(I),所以可得:

// 第一段 pre
M' = (M*T)*R = I*T*R = T*R

// 第二段 post
M' = T*(R*M) = T*R*I = T*R

由於兩者最終的化簡公式是相同的,所以兩者是等價的,但是,這結論不具備普適性。

即原始矩陣不為單位矩陣的時候,兩者無法化簡為相同的公式,結果自然也會不同。另外,執行順序就是程式書寫順序,不存在所謂的正序逆序。

錯誤結論二:pre 是先執行,而 post 是後執行。

這一條結論比上一條更離譜。

之所以產生這個錯誤完全是因為寫文章的人懂英語。

pre  :先,和 before 相似。
post :後,和 after  相似。

所以就得出了 pre 先執行,而 post 後執行這一說法,但從嚴謹的數學和程式角度來分析,完全是不可能的,還是上面所說的,pre 和 post 不能影響程式執行順序,而程式每執行一條語句都會得出一個確定的結果,所以,它根本不能控制先後執行,屬於完全扯淡型。

如果非要用這套理論強行解釋的話,反而看起來像是 post 先執行,例如:

matrix.preRotate(angle);
matrix.postTranslate(pivotX,pivotY);

同樣化簡公式:

// 矩陣乘法滿足結合律
M‘ = T*(M*R) = T*M*R = (T*M)*R

從實際上來說,由於矩陣乘法滿足結合律,所以不論你說是靠右先執行還是靠左先執行,從結果上來說都沒有錯。

之前基於這條錯誤的結論我進行了一次錯誤的證明:

(這段內容註定要成為我寫作歷程中不可抹滅的恥辱,既然是公開文章,就應該對讀者負責,雖然我在發表每一篇文章之前都竭力的求證其中的問題,各種細節,避免出現這種錯誤,但終究還是留下了這樣一段內容,在此我誠摯的向我所有的讀者道歉。)

關注我的讀者請儘量看我在 個人部落格GitHub 釋出的版本,這兩個平臺都在博文修復計劃之內,有任何錯誤或者紕漏,都會首先修復這兩個平臺的文章。另外,所有進行修復過的文章都會在我的微博 @GcsSloop 重新發布說明,關注我的微博可以第一時間得到博文更新或者修復的訊息。


以下是錯誤證明:

在實際操作中,我們每一步操作都會得出準確的計算結果,但是為什麼還會用存在先後的說法? 難道真的能夠用pre和post影響計算順序? 實則不然,下面我們用一個例子說明:

Matrix matrix = new Matrix();
matrix.postScale(0.5f, 0.8f);
matrix.preTranslate(1000, 1000);
Log.e(TAG, "MatrixTest" + matrix.toShortString());

在上面的操作中,如果按照正常的思路,先縮放,後平移,縮放操作執行在前,不會影響到後續的平移操作,但是執行結果卻發現平移距離變成了(500, 800)。

在上面例子中,計算順序是沒有問題的,先計算的縮放,然後計算的平移,而縮放影響到平移則是因為前一步縮放後的結果矩陣右乘了平移矩陣,這是符合矩陣乘法的運算規律的,也就是說縮放操作雖然在前卻影響到了平移操作,相當於先執行了平移操作,然後執行的縮放操作,因此才有pre操作會先執行,而post操作會後執行這一說法


上面的論證是完全錯誤的,因為可以輕鬆舉出反例:

Matrix matrix = new Matrix();
matrix.preScale(0.5f, 0.8f);
matrix.preTranslate(1000, 1000);
Log.e(TAG, "MatrixTest" + matrix.toShortString());

反例中,雖然將 postScale 改為了 preScale ,但兩者結果是完全相同的,所以先後論根本就是錯誤的。

他們結果相同是因為最終化簡公式是相同的,都是 S*T

之所以平移距離是 MTRANS_X = 500,MTRANS_Y = 800,那是因為執行 Translate 之前 Matrix 已經具有了一個縮放比例。在右乘的時候影響到了具體的數值計算,可以用矩陣乘法計算一下。

最終結果為:

當 T*S 的時候,縮放比例則不會影響到 MTRANS_X 和 MTRANS_Y ,具體可以使用矩陣乘法自己計算一遍。

如何理解和使用 pre 和 post ?

不要去管什麼先後論,順序論,就按照最基本的矩陣乘法理解。

pre  : 右乘, M‘ = M*A
post : 左乘, M’ = A*M

那麼如何使用?

正確使用方式就是先構造正常的 Matrix 乘法順序,之後根據情況使用 pre 和 post 來把這個順序實現。

還是用一個最簡單的例子理解,假設需要圍繞某一點旋轉。

可以用這個方法 xxxRotate(angle, pivotX, pivotY) ,由於我們這裡需要組合構造一個 Matrix,所以不直接使用這個方法。

首先,有兩條基本定理:

  • 所有的操作(旋轉、平移、縮放、錯切)預設都是以座標原點為基準點的。

  • 之前操作的座標系狀態會保留,並且影響到後續狀態。

基於這兩條基本定理,我們可以推算出要基於某一個點進行旋轉需要如下步驟:

1. 先將座標系原點移動到指定位置,使用平移 T
2. 對座標系進行旋轉,使用旋轉 S (圍繞原點旋轉)
3. 再將座標系平移回原來位置,使用平移 -T

具體公式如下:

M 為原始矩陣,是一個單位矩陣, M‘ 為結果矩陣, T 為平移, R為旋轉

M' = M*T*R*-T = T*R*-T

按照公式寫出來的虛擬碼如下:

Matrix matrix = new Matrix();
matrix.preTranslate(pivotX,pivotY);
matrix.preRotate(angle);
matrix.preTranslate(-pivotX, -pivotY);

圍繞某一點操作可以拓展為通用情況,即:

Matrix matrix = new Matrix();
matrix.preTranslate(pivotX,pivotY);
// 各種操作,旋轉,縮放,錯切等,可以執行多次。
matrix.preTranslate(-pivotX, -pivotY);

公式為:

M' = M*T* ... *-T = T* ... *-T

但是這種方式,兩個調整中心的平移函式就拉的太開了,所以通常採用這種寫法:

Matrix matrix = new Matrix();
// 各種操作,旋轉,縮放,錯切等,可以執行多次。
matrix.postTranslate(pivotX,pivotY);
matrix.preTranslate(-pivotX, -pivotY);

這樣公式為:

M' = T*M* ... *-T = T* ... *-T

可以看到最終化簡結果是相同的。

所以說,pre 和 post 就是用來調整乘法順序的,正常情況下應當正向進行構建出乘法順序公式,之後根據實際情況調整書寫即可。

在構造 Matrix 時,個人建議儘量使用一種乘法,前乘或者後乘,這樣操作順序容易確定,出現問題也比較容易排查。當然,由於矩陣乘法不滿足交換律,前乘和後乘的結果是不同的,使用時應結合具體情景分析使用。

下面我們用不同對方式來構造一個相同的矩陣:

注意:

  • 1.由於矩陣乘法不滿足交換律,請保證使用初始矩陣(Initial Matrix),否則可能導致運算結果不同。
  • 2.注意構造順序,順序是會影響結果的。
  • 3.Initial Matrix是指new出來的新矩陣,或者reset後的矩陣,是一個單位矩陣。

1.僅用pre:

// 使用pre, M' = M*T*S = T*S
Matrix m  new Matrix();
m.reset();
m.preTranslate(tx, ty); 
m.preScale(sx, sy);

用矩陣表示:

2.僅用post:

// 使用post, M‘ = T*S*M = T*S
Matrix m  new Matrix();
m.reset();
m.postScale(sx, sy);  //,越靠前越先執行。
m.postTranslate(tx, ty);

用矩陣表示:

3.混合:

// 混合 M‘ = T*M*S = T*S
Matrix m  new Matrix();
m.reset();
m.preScale(sx, sy);  
m.postTranslate(tx, ty);

或:

// 混合 M‘ = T*M*S = T*S
Matrix m  new Matrix();
m.reset();
m.postTranslate(tx, ty);
m.preScale(sx, sy);  

由於此處只有兩步操作,且指定了先後,所以程式碼上交換並不會影響結果。

用矩陣表示:

注意: 由於矩陣乘法不滿足交換律,請保證初始矩陣為單位矩陣,如果初始矩陣不為單位矩陣,則導致運算結果不同。

上面雖然用了很多不同的寫法,但最終的化簡公式是一樣的,這些不同的寫法,都是根據同一個公式反向推算出來的。

Matrix方法表

這個方法表,暫時放到這裡讓大家看看,方法的使用講解放在下一篇文章中。

方法類別 相關API 摘要
基本方法 equals hashCode toString toShortString 比較、 獲取雜湊值、 轉換為字串
數值操作 set reset setValues getValues 設定、 重置、 設定數值、 獲取數值
數值計算 mapPoints mapRadius mapRect mapVectors 計算變換後的數值
設定(set) setConcat setRotate setScale setSkew setTranslate 設定變換
前乘(pre) preConcat preRotate preScale preSkew preTranslate 前乘變換
後乘(post) postConcat postRotate postScale postSkew postTranslate 後乘變換
特殊方法 setPolyToPoly setRectToRect rectStaysRect setSinCos 一些特殊操作
矩陣相關 invert isAffine isIdentity 求逆矩陣、 是否為仿射矩陣、 是否為單位矩陣 …

總結

對於Matrix重在理解,理解了其中的原理之後用起來將會更加得心應手。

學完了本篇之後,推薦配合鴻洋大大的視訊課程 打造個性的圖片預覽與多點觸控 食用,定然能夠讓你對Matrix對理解更上一層樓。

由於個人水平有限,文章中可能會出現錯誤,如果你覺得哪一部分有錯誤,或者發現了錯別字等內容,歡迎在評論區告訴我,另外,據說關注 作者微博 不僅能第一時間收到新文章訊息,還能變帥哦。

About

本系列相關文章

作者微博: GcsSloop

參考資料

Matrix
Android中影象變換Matrix的原理、程式碼驗證和應用
Android中關於矩陣(Matrix)前乘後乘的一些認識
維基百科-仿射變換
維基百科-齊次座標
維基百科-線性對映
齊次座標系入門級思考
仿射變換與齊次座標