Android | 談一談 Matrix 與坐標變換

前言

  • Matrix是在Android源碼中出現(xiàn)頻率較高的工具類
  • 雖然Google已經(jīng)為我們屏蔽了很多數(shù)學細節(jié)茅主,所以使用者即使不了解Matrix的源碼與數(shù)學知識爆办,也不影響使用Matrix實現(xiàn)一些基本的效果
  • 但是理解Matrix的源碼與數(shù)學知識徽惋,對于理解Android相關的源碼能事半功倍

目錄


1. 矩陣數(shù)學基礎

矩陣相關的數(shù)學基礎知識總結(jié)如下表所示:


2. Matrix 使用步驟

現(xiàn)在我們將視線回到Matrix锨推,Matrix本質(zhì)上是一個利用矩陣運算實現(xiàn)坐標變換的工具類赘理,在Android很多地方可以看到它的身影宦言,我們以ImageView為例子介紹Matrix的使用步驟:

步驟1:創(chuàng)建矩陣

ImageView對象中有兩個Matrix成員變量:mMatrixmDrawMatrix,具體如下:

// ImageView.java

private Matrix mMatrix;
private Matrix mDrawMatrix;

// 在構造函數(shù)中調(diào)用
private void initImageView() {
    mMatrix = new Matrix();
    mScaleType = ScaleType.FIT_CENTER;
}

public void setImageMatrix(Matrix matrix) {
    // 省略部分代碼...
    // 分析點1:參數(shù) matrix 的值拷貝到 mMatrix
    mMatrix.set(matrix);
    // 分析點2:設置 mDrawMatrix
    configureBounds();
    // 重繪:觸發(fā)onDraw(Canvas)
    invalidate();
}

// Matrix.java

// 分析點1:參數(shù) matrix 的值拷貝到 mMatrix
public void set(Matrix src) {
    if (src == null) {
        reset();
    } else {
        // native 方法
        nSet(native_instance, src.native_instance);
    }
}

可以看到商模,mMatrixImageView的構造器中就創(chuàng)建了奠旺,另外ImageView還提供了setImageMatrix(Matrix)供外部設置蜘澜。那么mDrawMatrix是在哪里創(chuàng)建的呢?

// ImageView.java

// 分析點2:設置 mDrawMatrix
private void configureBounds() {
    // 省略部分代碼...
    if (ScaleType.CENTER == mScaleType) {
        mDrawMatrix = mMatrix;
        // 居中
        mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),Math.round((vheight - dheight) * 0.5f));
    }
    // 省略部分代碼...        
}

configureBounds()里有多個分支响疚,其中有些分支里將mMatrix賦值給mDrawMatrix鄙信,說明兩者是同一個對象。

步驟2:設置矩陣

創(chuàng)建矩陣之后忿晕,就可以使用Matrix提供的方法設置矩陣了装诡,例如上面的代碼在ScaleTypeScaleType.CENTER時使用setTranslate()設置為居中。

// ImageView.java

mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f), Math.round((vheight - dheight) * 0.5f)); 

當然了践盼,創(chuàng)建并設置好Matrix之后慎王,再使用ImageView#setImageMatrix()設置進來也可以達到同樣的效果。

步驟3:使用矩陣進行坐標變換

現(xiàn)在我們看使用mDrawMatrix的地方:

// ImageView.java

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // 省略部分代碼...
    if (mDrawMatrix != null) {
        // 分析點1:左乘mDrawMatrix
        canvas.concat(mDrawMatrix);
    }
    mDrawable.draw(canvas);
}

// Canvas.java

// 分析點1:左乘mDrawMatrix
public void concat(@Nullable Matrix matrix) {
    if (matrix != null) nConcat(mNativeCanvasWrapper, matrix.native_instance);
}

可以看到宏侍,ImageView#onDraw(Canvas)中對Canvas左乘mDrawMatrix赖淤,前面說到:矩陣左乘相當于一次坐標變換。我們通過下面一個簡單的例子展示了ImageView設置Matrix前后的效果:

// 圖一:未設置Matrix
iv.setBackgroundColor(0xFF999999.toInt())
iv.scaleType = ImageView.ScaleType.MATRIX
iv.setImageResource(R.color.colorAccent)

// 圖二:設置Matrix谅河,縮放到兩倍
val matrix = Matrix().apply {
    setScale(2F,2F)
}
iv.imageMatrix = matrix
坐標轉(zhuǎn)換前后對比 示意圖

在后續(xù)的文章里咱旱,我將專門寫一篇文章分享更多ImageView源碼的細節(jié),感興趣的同學點一點關注哦


3. Matrix 源碼分析

從這一節(jié)開始我們來閱讀Matrix的源碼绷耍,源碼中出現(xiàn)了native方法吐限,這意味著Matrix中的部分源碼是在native層實現(xiàn),具體分為:Matrix.h褂始、Matrix.cpp诸典、 Matrix.java

3.1 Java 層初始化

// Matrix.java
public final long native_instance;

// sizeof(SkMatrix) is 9 * sizeof(float) + uint32_t
private static final long NATIVE_ALLOCATION_SIZE = 40;

private static class NoImagePreloadHolder {
    // 單例
    public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
            Matrix.class.getClassLoader(), nGetNativeFinalizer(), NATIVE_ALLOCATION_SIZE);
}

// 從 Android 8 開始,使用 NativeAllocationRegistry 幫助回收 native 層內(nèi)存
public Matrix() {
    // 創(chuàng)建一個native層對象崎苗,具體為 SkMatrix
    native_instance = nCreate(0);
    NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, native_instance);
}

// 在 Android 8 之前
public Matrix() {
    native_instance = native_create(0);
}

// Java 層初始化
// ---------------------------------------------------------------------
// native 層初始化
private static native long nCreate(long nSrc_or_zero);

static jlong create(JNIEnv* env, jobject clazz, jlong srcHandle) {
    const SkMatrix* src = reinterpret_cast<SkMatrix*>(srcHandle);
    SkMatrix* obj = new SkMatrix();
    if (src)
        // 淺拷貝
        *obj = *src;
    else
        // 重置
        obj->reset();
    return reinterpret_cast<jlong>(obj);
}

Java層初始化要點如下:

  • Matrix構造器在native創(chuàng)建了一個SkMatrix對象狐粱,并通過reinterpret_cast強制轉(zhuǎn)換為long賦值給Java層的native_instance

  • MatrixJava層其實沒有太多操作胆数,真正完成任務的實體是native層的SkMatrix肌蜻。SKMatrix是 Skia 圖形引擎提供的用于完成坐標變換的 3 x 3 矩陣

  • Android 8開始必尼,使用NativeAllocationRegistry幫助回收 native 層內(nèi)存蒋搜。NativeAllocationRegistry綁定了Java層和native層的兩個對象,并標記內(nèi)存大小為 40字節(jié)判莉,為什么是 40 個字節(jié)呢豆挽?我們在源碼里尋找答案:SkMatrix.hSkMatrix.cpp

# 提示 #

NativeAllocationRegistry是用來幫助回收native層內(nèi)存的券盅,即當Java層對象被垃圾回收時帮哈,立即去釋放Native層的內(nèi)存,在Canvas渗饮、Bitmap等類中也有同樣的機制但汞,詳見文章:《Android | 帶你理解 NativeAllocationRegistry 的原理與設計思想》

3.2 native 層初始化

// SkMatrix.h

SK_BEGIN_REQUIRE_DENSE
class SK_API SkMatrix {
public:
    enum {
        kMScaleX, //!< horizontal scale factor
        kMSkewX,  //!< horizontal skew factor
        kMTransX, //!< horizontal translation
        kMSkewY,  //!< vertical skew factor
        kMScaleY, //!< vertical scale factor
        kMTransY, //!< vertical translation
        kMPersp0, //!< input x perspective factor
        kMPersp1, //!< input y perspective factor
        kMPersp2, //!< perspective bias
    };
    
    // 分析點1:
    SkScalar get(int index) const {
        SkASSERT((unsigned)index < 9);
        return fMat[index];
    }
    // 分析點2:重置
    void reset();

    // 判斷是否為單位矩陣宿刮,使用單位矩陣進行矩陣乘法是無效的
    bool isIdentity() const {
        return this->getType() == 0;
    }

private:
    SkScalar         fMat[9];
    mutable uint32_t fTypeMask;
// SkMatrix.cpp

// 分析點2:重置為單位矩陣
void SkMatrix::reset() {
    fMat[kMScaleX] = fMat[kMScaleY] = fMat[kMPersp2] = 1;
    fMat[kMSkewX] = fMat[kMSkewY] =
    fMat[kMTransX] = fMat[kMTransY] =
    fMat[kMPersp0] = fMat[kMPersp1] = 0;
    this->setTypeMask(kIdentity_Mask | kRectStaysRect_Mask);
}
// SkScalar.h

typedef float SkScalar;

native層初始化要點如下:

  • SkMatrix有兩個字段:大小為 9 的數(shù)組fMatunit21_tfTypeMask,其中SkScalar其實是一個float私蕾,具體可以查看:SkScalar.h僵缺,現(xiàn)在你知道 40個字節(jié)(8 * 4 + 4 = 40)是如何的來了嗎?

  • SkMatrix邏輯上是一個 3 x 3 矩陣踩叭,物理上是一個1 x 9 數(shù)組

  • 初始化時會調(diào)用reset()磕潮,設置為單位矩陣,注意:使用單位矩陣進行矩陣乘法是無效的

\begin{bmatrix} 1&0&0\\ 0&1&0\\ 0&0&1\\ \end{bmatrix}

3.3 設置矩陣

前面我們理解了Matrix初始化時是一個單位矩陣容贝,現(xiàn)在我們開始為矩陣的元素賦值自脯。從Java層源碼可以看到,Matrix的方法主要分為setXXX()斤富、preXXX()postXXX()三大類膏潮,這三類方法有什么區(qū)別呢?我們以scale為例:

// Matrix.java

// set
public void setScale(float sx, float sy, float px, float py) {
    nSetScale(native_instance, sx, sy, px, py);
}
// 左乘
public boolean preScale(float sx, float sy, float px, float py) {
    nPreScale(native_instance, sx, sy, px, py);
    return true;
}
// 右乘
public boolean postScale(float sx, float sy, float px, float py) {
    nPostScale(native_instance, sx, sy, px, py);
    return true;
}

// Java 層
// ---------------------------------------------------------------------
// native 層

// Matrix.cpp
// Matrix 中本質(zhì)上使用了SkMatrix满力,這里省略...

// SkMatrix.cpp

void SkMatrix::setScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py) {
    if (1 == sx && 1 == sy) {
        this->reset();
    } else {
        this->setScaleTranslate(sx, sy, px - sx * px, py - sy * py);
    }
}

// | sx  0 tx |
// |  0 sy ty |
// |  0  0  1 |
void setScaleTranslate(SkScalar sx, SkScalar sy, SkScalar tx, SkScalar ty) {
    // 省略...
}

void SkMatrix::preScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py) {
    if (1 == sx && 1 == sy) {
        return;
    }
    // 1. 棧中分配一個SkMatrix對象
    SkMatrix m;
    // 2. 先調(diào)用setScale
    m.setScale(sx, sy, px, py);
    // 3. 兩個矩陣乘法
    this->preConcat(m);
}

void SkMatrix::postScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py) {
    if (1 == sx && 1 == sy) {
        return;
    }
    // 1. 棧中分配一個SkMatrix對象
    SkMatrix m;
    // 2. 先調(diào)用setScale
    m.setScale(sx, sy, px, py);
    // 3. 兩個矩陣乘法
    this->postConcat(m);
}

void SkMatrix::preConcat(const SkMatrix& mat) {
    if(!mat.isIdentity()) {
    this->setConcat(*this, mat);
    }
}
    
void SkMatrix::postConcat(const SkMatrix& mat) {
    if (!mat.isIdentity()) {
        this->setConcat(mat, *this);
    }
}

要點如下:

  • setScale()設置了矩陣的縮放屬性偏移屬性焕参,而其他屬性被清除
  • preScale()先在棧中分配一個新的SkMatrix,并執(zhí)行左乘:NEW x CUR
  • postScale()先在棧中分配一個新的SkMatrix油额,并執(zhí)行右邊乘:CUR x NEW

4. 總結(jié)

  • 關于Matrix的要點已經(jīng)在前面的內(nèi)容中列舉叠纷,這里就不再重復了;
  • 在后續(xù)的文章里潦嘶,我將與你探討ImageView源碼的細節(jié)涩嚣,并實現(xiàn)高仿微信圖片查看控件,歡迎關注 彭旭銳 的博客掂僵。

參考資料

推薦閱讀

感謝喜歡!你的點贊是對我最大的鼓勵互妓!歡迎關注彭旭銳的簡書!

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坤塞,一起剝皮案震驚了整個濱河市冯勉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌摹芙,老刑警劉巖灼狰,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異浮禾,居然都是意外死亡交胚,警方通過查閱死者的電腦和手機份汗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝴簇,“玉大人杯活,你說我怎么就攤上這事“敬剩” “怎么了旁钧?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長互拾。 經(jīng)常有香客問我歪今,道長,這世上最難降的妖魔是什么颜矿? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任寄猩,我火速辦了婚禮,結(jié)果婚禮上骑疆,老公的妹妹穿的比我還像新娘焦影。我一直安慰自己,他們只是感情好封断,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布斯辰。 她就那樣靜靜地躺著,像睡著了一般坡疼。 火紅的嫁衣襯著肌膚如雪彬呻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天柄瑰,我揣著相機與錄音闸氮,去河邊找鬼。 笑死教沾,一個胖子當著我的面吹牛蒲跨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播授翻,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼或悲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了堪唐?” 一聲冷哼從身側(cè)響起巡语,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎淮菠,沒想到半個月后男公,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡合陵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年枢赔,在試婚紗的時候發(fā)現(xiàn)自己被綠了澄阳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡踏拜,死狀恐怖碎赢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情执隧,我是刑警寧澤揩抡,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站镀琉,受9級特大地震影響峦嗤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜屋摔,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一烁设、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钓试,春花似錦装黑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挽鞠,卻和暖如春疚颊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背信认。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工材义, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嫁赏。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓其掂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親潦蝇。 傳聞我的和親對象是個殘疾皇子款熬,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345