Android | Matrix 与 坐标变换

作者: 彭旭锐 | 来源:发表于2020-06-22 18:21 被阅读0次

    前言

    • Matrix是在Android源码中出现频率较高的工具类
    • 虽然Google已经为我们屏蔽了很多数学细节,所以使用者即使不了解Matrix的源码与数学知识,也不影响使用Matrix实现一些基本的效果
    • 但是理解Matrix的源码与数学知识,对于理解Android相关的源码能事半功倍

    目录


    1. 矩阵数学基础

    矩阵相关的数学基础知识总结如下表所示:


    2. Matrix 使用步骤

    现在我们将视线回到MatrixMatrix本质上是一个利用矩阵运算实现坐标变换的工具类,在Android很多地方可以看到它的身影,我们以ImageView为例子介绍Matrix的使用步骤:

    步骤1:创建矩阵

    ImageView对象中有两个Matrix成员变量:mMatrixmDrawMatrix,具体如下:

    // ImageView.java
    
    private Matrix mMatrix;
    private Matrix mDrawMatrix;
    
    // 在构造函数中调用
    private void initImageView() {
        mMatrix = new Matrix();
        mScaleType = ScaleType.FIT_CENTER;
    }
    
    public void setImageMatrix(Matrix matrix) {
        // 省略部分代码...
        // 分析点1:参数 matrix 的值拷贝到 mMatrix
        mMatrix.set(matrix);
        // 分析点2:设置 mDrawMatrix
        configureBounds();
        // 重绘:触发onDraw(Canvas)
        invalidate();
    }
    
    // Matrix.java
    
    // 分析点1:参数 matrix 的值拷贝到 mMatrix
    public void set(Matrix src) {
        if (src == null) {
            reset();
        } else {
            // native 方法
            nSet(native_instance, src.native_instance);
        }
    }
    

    可以看到,mMatrixImageView的构造器中就创建了,另外ImageView还提供了setImageMatrix(Matrix)供外部设置。那么mDrawMatrix是在哪里创建的呢?

    // 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:设置矩阵

    创建矩阵之后,就可以使用Matrix提供的方法设置矩阵了,例如上面的代码在ScaleTypeScaleType.CENTER时使用setTranslate()设置为居中。

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

    当然了,创建并设置好Matrix之后,再使用ImageView#setImageMatrix()设置进来也可以达到同样的效果。

    步骤3:使用矩阵进行坐标变换

    现在我们看使用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
    
    坐标转换前后对比 示意图

    💓在后续的文章里,我将专门写一篇文章分享更多ImageView源码的细节,感兴趣的同学点一点关注哦 💓


    3. Matrix 源码分析

    从这一节开始我们来阅读Matrix的源码,源码中出现了native方法,这意味着Matrix中的部分源码是在native层实现,具体分为:Matrix.hMatrix.cppMatrix.java

    3.1 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);
    }
    
    public Matrix() {
        // 创建一个native层对象,具体为 SkMatrix
        native_instance = nCreate(0);
        NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, native_instance);
    }
    
    // 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创建了一个SkMatrix对象,并通过reinterpret_cast强制转换为long赋值给Java层的native_instance

    • MatrixJava层其实没有太多操作,真正完成任务的实体是native层的SkMatrixSKMatrix是 Skia 图形引擎提供的用于完成坐标变换的 3 x 3 矩阵

    • 使用NativeAllocationRegistry绑定了Java层和native层的两个对象,并标记内存大小为 40字节,为什么是 40 个字节呢?我们在源码里寻找答案:SkMatrix.hSkMatrix.cpp

    # 提示 #

    NativeAllocationRegistry是用来帮助回收native层内存的,即当Java层对象被释放则立即去释放Native层的对象内存,在CanvasBitmap等类中也有同样的机制,详见文章:Android | 对比Android M 前后的 NativeAllocationRegistry 变化 (Editting...)

    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 的数组fMatunit21_tfTypeMask,其中SkScalar其实是一个float,具体可以查看:SkScalar.h,现在你知道 40个字节(8 * 4 + 4 = 40)是如何的来了吗?

    • SkMatrix逻辑上是一个 3 x 3 矩阵,物理上是一个1 x 9 数组

    • 初始化时会调用reset(),设置为单位矩阵,注意:使用单位矩阵进行矩阵乘法是无效的

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

    3.3 设置矩阵

    前面我们理解了Matrix初始化时是一个单位矩阵,现在我们开始为矩阵的元素赋值。从Java层源码可以看到,Matrix的方法主要分为setXXX()preXXX()postXXX()三大类,这三类方法有什么区别呢?我们以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 中本质上使用了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. 先调用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. 先调用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,并执行左乘:NEW x CUR
    • postScale()先在栈中分配一个新的SkMatrix,并执行右边乘:CUR x NEW

    4. 总结

    • 关于Matrix的要点已经在前面的内容中列举,这里就不再重复了;
    • 在后续的文章里,我将与你探讨ImageView源码的细节,并实现高仿微信图片查看控件,欢迎关注 彭旭锐 的博客。

    参考资料

    推荐阅读

    感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的简书!

    相关文章

      网友评论

        本文标题:Android | Matrix 与 坐标变换

        本文链接:https://www.haomeiwen.com/subject/nmxmuhtx.html