自定義View進階篇《十》——Matrix詳解

weixin_33670713發表於2017-04-14

這應該是目前最詳細的一篇講解Matrix的中文文章了,在上一篇文章Matrix原理中,我們對Matrix做了一個簡單的瞭解,偏向理論,在本文中則會詳細的講解Matrix的具體用法,以及與Matrix相關的一些實用技巧。

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

Matrix方法表

按照慣例,先放方法表做概覽。


5527952-4f41a305f7c3ee6c.png

Matrix方法詳解

構造方法

構造方法沒有在上面表格中列出。

無參構造
Matrix ()

建立一個全新的Matrix,使用格式如下:

Matrix matrix = new Matrix();

通過這種方式建立出來的並不是一個數值全部為空的矩陣,而是一個單位矩陣,如下:


5527952-2a10ed634235454d.png
有參構造
Matrix (Matrix src)

這種方法則需要一個已經存在的矩陣作為引數,使用格式如下:

Matrix matrix = new Matrix(src);

建立一個Matrix,並對src深拷貝(理解為新的matrix和src是兩個物件,但內部數值相同即可)。

基本方法

基本方法內容比較簡單,在此處簡要介紹一下。

1.equals

比較兩個Matrix的數值是否相同。

2.hashCode

獲取Matrix的雜湊值。

3.toString

將Matrix轉換為字串: Matrix{[1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]}

4.toShortString

將Matrix轉換為短字串: [1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]

數值操作

數值操作這一組方法可以幫助我們直接控制Matrix裡面的數值。

1.set
void set (Matrix src)

沒有返回值,有一個引數,作用是將引數Matrix的數值複製到當前Matrix中。如果引數為空,則重置當前Matrix,相當於reset()。

2.reset
void reset ()

重置當前Matrix(將當前Matrix重置為單位矩陣)。

3.setValues
void setValues (float[] values)

setValues的引數是浮點型的一維陣列,長度需要大於9,拷貝陣列中的前9位數值賦值給當前Matrix。

4.getValues
void getValues (float[] values)

很顯然,getValues和setValues是一對方法,引數也是浮點型的一維陣列,長度需要大於9,將Matrix中的數值拷貝進引數的前9位中。

數值計算
1.mapPoints
void mapPoints (float[] pts)

void mapPoints (float[] dst, float[] src)

void mapPoints (float[] dst, int dstIndex,float[] src, int srcIndex, int pointCount)

計算一組點基於當前Matrix變換後的位置,(由於是計算點,所以引數中的float陣列長度一般都是偶數的,若為奇數,則最後一個數值不參與計算)。

它有三個過載方法:

(1) void mapPoints (float[] pts) 方法僅有一個引數,pts陣列作為引數傳遞原始數值,計算結果仍存放在pts中。

示例:

// 初始資料為三個點 (0, 0) (80, 100) (400, 300) 
float[] pts = new float[]{0, 0, 80, 100, 400, 300};

// 構造一個matrix,x座標縮放0.5
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);

// 輸出pts計算之前資料
Log.i(TAG, "before: "+ Arrays.toString(pts));

// 呼叫map方法計算
matrix.mapPoints(pts);

// 輸出pts計算之後資料
Log.i(TAG, "after : "+ Arrays.toString(pts));

結果:

before: [0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
after : [0.0, 0.0, 40.0, 100.0, 200.0, 300.0]

(2) void mapPoints (float[] dst, float[] src) ,src作為引數傳遞原始數值,計算結果存放在dst中,src不變。

如果原始資料需要保留則一般使用這種方法。

示例:

// 初始資料為三個點 (0, 0) (80, 100) (400, 300)
float[] src = new float[]{0, 0, 80, 100, 400, 300};
float[] dst = new float[6];

// 構造一個matrix,x座標縮放0.5
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);

// 輸出計算之前資料
Log.i(TAG, "before: src="+ Arrays.toString(src));
Log.i(TAG, "before: dst="+ Arrays.toString(dst));

// 呼叫map方法計算
matrix.mapPoints(dst,src);

// 輸出計算之後資料
Log.i(TAG, "after : src="+ Arrays.toString(src));
Log.i(TAG, "after : dst="+ Arrays.toString(dst));

結果:

before: src=[0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
before: dst=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
after : src=[0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
after : dst=[0.0, 0.0, 40.0, 100.0, 200.0, 300.0]

(3) void mapPoints (float[] dst, int dstIndex,float[] src, int srcIndex, int pointCount) 可以指定只計算一部分數值。


5527952-541c076f9f0e115e.png

示例:
將第二、三個點計算後儲存進dst最開始位置。

// 初始資料為三個點 (0, 0) (80, 100) (400, 300)
float[] src = new float[]{0, 0, 80, 100, 400, 300};
float[] dst = new float[6];

// 構造一個matrix,x座標縮放0.5
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);

// 輸出計算之前資料
Log.i(TAG, "before: src="+ Arrays.toString(src));
Log.i(TAG, "before: dst="+ Arrays.toString(dst));

// 呼叫map方法計算(最後一個2表示兩個點,即四個數值,並非兩個數值)
matrix.mapPoints(dst, 0, src, 2, 2);

// 輸出計算之後資料
Log.i(TAG, "after : src="+ Arrays.toString(src));
Log.i(TAG, "after : dst="+ Arrays.toString(dst));

結果:

before: src=[0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
before: dst=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
after : src=[0.0, 0.0, 80.0, 100.0, 400.0, 300.0]
after : dst=[40.0, 100.0, 200.0, 300.0, 0.0, 0.0]
2.mapRadius
float mapRadius (float radius)

測量半徑,由於圓可能會因為畫布變換變成橢圓,所以此處測量的是平均半徑。

示例:

float radius = 100;
float result = 0;

// 構造一個matrix,x座標縮放0.5
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);

Log.i(TAG, "mapRadius: "+radius);

result = matrix.mapRadius(radius);

Log.i(TAG, "mapRadius: "+result);

結果:

mapRadius: 100.0
mapRadius: 70.71068
3.mapRect
boolean mapRect (RectF rect)

boolean mapRect (RectF dst, RectF src)

測量矩形變換後位置。
(1) boolean mapRect (RectF rect) 測量rect並將測量結果放入rect中,返回值是判斷矩形經過變換後是否仍為矩形。
示例:

RectF rect = new RectF(400, 400, 1000, 800);

// 構造一個matrix
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);
matrix.postSkew(1,0);

Log.i(TAG, "mapRadius: "+rect.toString());

boolean result = matrix.mapRect(rect);

Log.i(TAG, "mapRadius: "+rect.toString());
Log.e(TAG, "isRect: "+ result);

結果:

mapRadius: RectF(400.0, 400.0, 1000.0, 800.0)
mapRadius: RectF(600.0, 400.0, 1300.0, 800.0)
isRect: false

由於使用了錯切,所以返回結果為false。

(2) boolean mapRect (RectF dst, RectF src) 測量src並將測量結果放入dst中,返回值是判斷矩形經過變換後是否仍為矩形,和之前沒有什麼太大區別,此處就不囉嗦了。

4.mapVectors

測量向量。

void mapVectors (float[] vecs)

void mapVectors (float[] dst, float[] src)

void mapVectors (float[] dst, int dstIndex, float[] src, int srcIndex, int vectorCount)

mapVectors 與 mapPoints 基本上是相同的,可以直接參照上面的mapPoints使用方法。

而兩者唯一的區別就是mapVectors不會受到位移的影響,這符合向量的定律,如果你不瞭解的話,請找到以前教過你的老師然後把學費要回來。

區別:

float[] src = new float[]{1000, 800};
float[] dst = new float[2];

// 構造一個matrix
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 1f);
matrix.postTranslate(100,100);

// 計算向量, 不受位移影響
matrix.mapVectors(dst, src);
Log.i(TAG, "mapVectors: "+Arrays.toString(dst));

// 計算點
matrix.mapPoints(dst, src);
Log.i(TAG, "mapPoints: "+Arrays.toString(dst));

結果:

mapVectors: [500.0, 800.0]
mapPoints: [600.0, 900.0]
set、pre 與 post

對於四種基本變換 平移(translate)、縮放(scale)、旋轉(rotate)、 錯切(skew) 它們每一種都三種操作方法,分別為 設定(set)、 前乘(pre) 和 後乘 (post)。而它們的基礎是Concat,通過先構造出特殊矩陣然後用原始矩陣Concat特殊矩陣,達到變換的結果。
關於四種基本變換的知識和三種對應操作的區別,詳細可以參考 Canvas之畫布操作 和 Matrix原理 這兩篇文章的內容。

由於之前的文章已經詳細的講解過了它們的原理與用法,所以此處就簡要的介紹一下:


5527952-e183af3d10c5342f.png

Matrix 相關的重要知識:

  • 1.一開始從Canvas中獲取到到Matrix並不是初始矩陣,而是經過偏移後到矩陣,且偏移距離就是距離螢幕左上角的位置。
    這個可以用於判定View在螢幕上的絕對位置,View可以根據所處位置做出調整。
  • 2.構造Matrix時使用的是矩陣乘法,前乘(pre)與後乘(post)結果差別很大。
  • 3.受矩陣乘法影響,後面的執行的操作可能會影響到之前的操作。
    使用時需要注意構造順序。
特殊方法

這一類方法看似不起眼,但拿來稍微加工一下就可能製作意想不到的效果。

1.setPolyToPoly
boolean setPolyToPoly (
        float[] src,    // 原始陣列 src [x,y],儲存內容為一組點
        int srcIndex,   // 原始陣列開始位置
        float[] dst,    // 目標陣列 dst [x,y],儲存內容為一組點
        int dstIndex,   // 目標陣列開始位置
        int pointCount) // 測控點的數量 取值範圍是: 0到4

Poly全稱是Polygon,多邊形的意思,瞭解了意思大致就能知道這個方法是做什麼用的了,應該與PS中自由變換中的扭曲有點類似。


5527952-82efd7e39b45739b.gif

從引數我們可以瞭解到setPolyToPoly最多可以支援4個點,這四個點通常為圖形的四個角,可以通過這四個角將檢視從矩形變換成其他形狀。

簡單示例:

public class MatrixSetPolyToPolyTest extends View {

    private Bitmap mBitmap;             // 要繪製的圖片
    private Matrix mPolyMatrix;         // 測試setPolyToPoly用的Matrix

    public MatrixSetPolyToPolyTest(Context context) {
        super(context);

        initBitmapAndMatrix();
    }

    private void initBitmapAndMatrix() {
        mBitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.poly_test);

        mPolyMatrix = new Matrix();


        float[] src = {0, 0,                                    // 左上
                mBitmap.getWidth(), 0,                          // 右上
                mBitmap.getWidth(), mBitmap.getHeight(),        // 右下
                0, mBitmap.getHeight()};                        // 左下

        float[] dst = {0, 0,                                    // 左上
                mBitmap.getWidth(), 400,                        // 右上
                mBitmap.getWidth(), mBitmap.getHeight() - 200,  // 右下
                0, mBitmap.getHeight()};                        // 左下

        // 核心要點
        mPolyMatrix.setPolyToPoly(src, 0, dst, 0, src.length >> 1); // src.length >> 1 為位移運算 相當於處以2

        // 此處為了更好的顯示對圖片進行了等比縮放和平移(圖片本身有點大)
        mPolyMatrix.postScale(0.26f, 0.26f);
        mPolyMatrix.postTranslate(0,200);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 根據Matrix繪製一個變換後的圖片
        canvas.drawBitmap(mBitmap, mPolyMatrix, null);
    }
}
5527952-9ed17af0c5c1e801.jpg

我們知道pointCount支援點的個數為0到4個,四個一般指圖形的四個角,屬於最常用的一種情形,但前面幾種是什麼情況呢?


5527952-fc68a882ce67d52c.png

從上表我們可以觀察出一個規律, 隨著pointCount數值增大setPolyToPoly的可以操作性也越來越強,這不是廢話麼,可調整點數多了能幹的事情自然也多了。

測控點選取位置?

測控點可以選擇任何你認為方便的位置,只要src與dst一一對應即可。不過為了方便,通常會選擇一些特殊的點: 圖形的四個角,邊線的中心點以及圖形的中心點等。不過有一點需要注意,測控點選取都應當是不重複的(src與dst均是如此),如果選取了重複的點會直接導致測量失效,這也意味著,你不允許將一個方形(四個點)對映為三角形(四個點,但其中兩個位置重疊),但可以接近於三角形。

作用範圍?

作用範圍當然是設定了Matrix的全部區域,如果你將這個Matrix賦值給了Canvas,它的作用範圍就是整個畫布,如果你賦值給了Bitmap,它的作用範圍就是整張圖片。
接下來用示例演示一下,所有示例的src均為圖片大小,dst根據手勢變化。

pointCount為0

pointCount為0和reset是等價的,而不是保持matrix不變,在最底層的實現中可以看到這樣的程式碼:

if (0 == count) {
    this->reset();
    return true;
}
5527952-58e8d9578730a47e.gif
pointCount為1

pointCount為1和translate是等價的,在最底層的實現中可以看到這樣的程式碼:

if (1 == count) {
    this->setTranslate(dst[0].fX - src[0].fX, dst[0].fY - src[0].fY);
    return true;
}

平移的距離是dst - src.

當測控點為1的時候,由於你只有一個點可以控制,所以你只能拖拽著它在2D平面上滑動。


5527952-1452bca63201c705.gif
pointCount為2

當pointCount為2的時候,可以做縮放、平移和旋轉。


5527952-6ca567997685950a.gif
pointCount為3

當pointCount為3的時候,可以做縮放、平移、旋轉和錯切。


5527952-0a08eb31c1172d94.gif
pointCount為4

當pointCount為4的時候,你可以將影象拉伸為任意四邊形。

5527952-6d03a75bb281ed6e.gif

上面已經用圖例比較詳細的展示了不同操控點個數的情況,如果你依舊存在疑問,可以獲取程式碼自己試一下。
點選此處檢視setPolyToPoly測試程式碼

2.setRectToRect
boolean setRectToRect (RectF src,           // 源區域
                RectF dst,                  // 目標區域
                Matrix.ScaleToFit stf)      // 縮放適配模式

簡單來說就是將源矩形的內容填充到目標矩形中,然而在大多數的情況下,源矩形和目標矩形的長寬比是不一致的,到底該如何填充呢,這個填充的模式就由第三個引數 stf 來確定。

ScaleToFit 是一個列舉型別,共包含了四種模式:


5527952-b2112446949855df.png

下面我們看一下不同寬高比的src與dst在不同模式下是怎樣的。
假設灰色部分是dst,橙色部分是src,由於是測試不同寬高比,示例中讓dst保持不變,看兩種寬高比的src在不同模式下填充的位置。


5527952-88d7850c5a810c19.png

下面用程式碼演示一下居中的示例:
public class MatrixSetRectToRectTest extends View {

    private static final String TAG = "MatrixSetRectToRectTest";

    private int mViewWidth, mViewHeight;

    private Bitmap mBitmap;             // 要繪製的圖片
    private Matrix mRectMatrix;         // 測試etRectToRect用的Matrix

    public MatrixSetRectToRectTest(Context context) {
        super(context);

        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.rect_test);
        mRectMatrix = new Matrix();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        RectF src= new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight() );
        RectF dst = new RectF(0, 0, mViewWidth, mViewHeight );

        // 核心要點
        mRectMatrix.setRectToRect(src,dst, Matrix.ScaleToFit.CENTER);

        // 根據Matrix繪製一個變換後的圖片
        canvas.drawBitmap(mBitmap, mRectMatrix, new Paint());
    }
}
5527952-45746e50b3aa3b4e.jpg
3.rectStaysRect

判斷矩形經過變換後是否仍為矩形,假如Matrix進行了平移、縮放則畫布僅僅是位置和大小改變,矩形變換後仍然為矩形,但Matrix進行了非90度倍數的旋轉或者錯切,則矩形變換後就不再是矩形了,這個很好理解,不過多贅述,順便說一下,前面的mapRect方法的返回值就是根據rectStaysRect來判斷的。

4.setSinCos

設定sinCos值,這個是控制Matrix旋轉的,由於Matrix已經封裝好了Rotate方法,所以這個並不常用,在此僅作概述。

// 方法一
void setSinCos (float sinValue,     // 旋轉角度的sin值
                float cosValue)     // 旋轉角度的cos值

// 方法二
void setSinCos (float sinValue,     // 旋轉角度的sin值
                float cosValue,     // 旋轉角度的cos值
                float px,           // 中心位置x座標
                float py)           // 中心位置y座標

簡單測試:

Matrix matrix = new Matrix();
// 旋轉90度
// sin90=1
// cos90=0
matrix.setSinCos(1f, 0f);

Log.i(TAG, "setSinCos:"+matrix.toShortString());

// 重置
matrix.reset();

// 旋轉90度
matrix.setRotate(90);

Log.i(TAG, "setRotate:"+matrix.toShortString());

結果:

setSinCos:[0.0, -1.0, 0.0][1.0, 0.0, 0.0][0.0, 0.0, 1.0]
setRotate:[0.0, -1.0, 0.0][1.0, 0.0, 0.0][0.0, 0.0, 1.0]
矩陣相關

矩陣相關的函式就屬於哪一種非常靠近底層的東西了,大部分開發者很少直接接觸這些東西,想要弄明白這個可以回去請教你們的線性代數老師,這裡也僅作概述。


5527952-71115e3b22dbc3ff.png
1.invert

求矩陣的逆矩陣,簡而言之就是計算與之前相反的矩陣,如果之前是平移200px,則求的矩陣為反向平移200px,如果之前是縮小到0.5f,則結果是放大到2倍。

boolean invert (Matrix inverse)

簡單測試:

Matrix matrix = new Matrix();
Matrix invert = new Matrix();
matrix.setTranslate(200,500);

Log.e(TAG, "before - matrix "+matrix.toShortString() );

Boolean result = matrix.invert(invert);

Log.e(TAG, "after  - result "+result );
Log.e(TAG, "after  - matrix "+matrix.toShortString() );
Log.e(TAG, "after  - invert "+invert.toShortString() );

結果:

before - matrix [1.0, 0.0, 200.0][0.0, 1.0, 500.0][0.0, 0.0, 1.0]
after  - result true
after  - matrix [1.0, 0.0, 200.0][0.0, 1.0, 500.0][0.0, 0.0, 1.0]
after  - invert [1.0, 0.0, -200.0][0.0, 1.0, -500.0][0.0, 0.0, 1.0]
2.isAffine

判斷矩陣是否是仿射矩陣, 貌似並沒有太大卵用,因為你無論如何操作結果始終都為true。

這是為什麼呢?因為迄今為止我們使用的所有變換都是仿射變換,那變換出來的矩陣自然是仿射矩陣嘍。

判斷是否是仿射矩陣最重要的一點就是,直線是否仍為直線,簡單想一下就知道,不論平移,旋轉,錯切,縮放,直線變換後最終仍為直線,要想讓isAffine的結果變為false,除非你能把直線掰彎,我目前還沒有找到能夠掰彎的方法,所以我仍是直男(就算找到了,我依舊是直男)。

簡單測試:

Matrix matrix = new Matrix();
Log.i(TAG,"isAffine="+matrix.isAffine());

matrix.postTranslate(200,0);
matrix.postScale(0.5f, 1);
matrix.postSkew(0,1);
matrix.postRotate(56);

Log.i(TAG,"isAffine="+matrix.isAffine());

結果:

isAffine=true
isAffine=true

3.isIdentity
判斷是否為單位矩陣,什麼是單位矩陣呢,就是文章一開始的那個:


5527952-df5cc0e4cb12e9c9.png

新建立的Matrix和重置後的Matrix都是單位矩陣,不過,只要隨意操作一步,就不在是單位矩陣了。
簡單測試:

Matrix matrix = new Matrix();
Log.i(TAG,"isIdentity="+matrix.isIdentity());

matrix.postTranslate(200,0);

Log.i(TAG,"isIdentity="+matrix.isIdentity());

結果:

isIdentity=true
isIdentity=false
Matrix實用技巧

通過前面的程式碼和示例,我們已經瞭解了Matrix大部分方法是如何使用的,這些基本的原理和方法通過組合可能會創造出神奇的東西,網上有很多教程講Bitmap利用Matrix變換來製作映象倒影等,這都屬於Matrix的基本應用,我就不在贅述了,下面我簡要介紹幾種然並卵的小技巧,更多的大家可以開啟自己的腦洞來發揮。

1.獲取View在螢幕上的絕對位置

在之前的文章Matrix原理中我們提到過Matrix最根本的作用就是座標對映,將View的相對座標對映為螢幕的絕對座標,也提到過我們在onDraw函式的canvas中獲取到到Matrix並不是單位矩陣,結合這兩點,聰明的你肯定想到了我們可以從canvas的Matrix入手取得View在螢幕上的絕對位置。
不過,這也僅僅是一個然並卵的小技巧而已,使用getLocationOnScreen
同樣可以獲取View在螢幕的位置,但如果你是想讓下一任接盤俠弄不明白你在做什麼或者是被同事打死的話,儘管這麼做。
簡單示例:

@Override
protected void onDraw(Canvas canvas) {
    float[] values = new float[9];
    int[] location1 = new int[2];

    Matrix matrix = canvas.getMatrix();
    matrix.getValues(values);

    location1[0] = (int) values[2];
    location1[1] = (int) values[5];
    Log.i(TAG, "location1 = " + Arrays.toString(location1));

    int[] location2 = new int[2];
    this.getLocationOnScreen(location2);
    Log.i(TAG, "location2 = " + Arrays.toString(location2));
}

結果:

location1 = [0, 243]
location2 = [0, 243]
2.利用setPolyToPoly製造3D效果

這個全憑大家想象力啦,不過我搜了一下還真搜到了好東西,之前鴻洋大大發過一篇博文詳細講解了利用setPolyToPoly製造的摺疊效果佈局,大家直接到他的部落格去看吧,我就不寫了。

圖片引用自鴻洋大大的部落格,稍作了一下處理。

5527952-d5c00de589543b45.gif
博文連結:

Android FoldingLayout 摺疊佈局 原理及實現(一)
Android FoldingLayout 摺疊佈局 原理及實現(二)

相關文章