android圖片裁剪拼接實現(一):Matrix基本使用

Kongdy發表於2017-12-21

一、前文

  之前有個朋友委託我實現一個圖片拼接的元件,感覺挺有意思,於是週末花了些時間去研究了下,其實拼接這一步並不難,但是我在研究中發現了Matrix這個東西,非常好的東西。為此,我竟然拾起了多年沒有動過的線性代數。

二、原理

  要徹底搞懂matrix還是需要一定的線性代數上面的理解,不過對於基本使用,瞭解到矩陣乘法就足夠了。
  在android座標系中,分為x、y和z三個軸,分別代表了長、寬、高三個維度。如下圖所示

android座標系

  在android中,使用三維座標(x,y,z)組成一個行列式與一個三階行列式進行矩陣乘法。
矩陣乘法

  圖中顯示的使用初始座標組成的矩陣與單位矩陣進行矩陣乘法。矩陣乘法使用可以參考矩陣乘法
  Martix會把輸入進來的矩陣帶入到其內部的矩陣中進行計算,最終輸出新的矩陣,來達到對圖形形態的處理。

三、基本方法的使用

  Matrix提供的基本方法有三種模式,

  1. setXXX()方法,例如 setRotate(),setScale()
  2. preXXX()方法,例如 preRotate(),preScale()
  3. postXXX()方法,例如 postRotate(),postScale()
    其中,setXXX()會先將矩陣重置為單位矩陣,然後再進行矩陣變幻
    preXXX()和postXXX()方法會牽扯到矩陣的前乘和後乘,如果瞭解了矩陣乘法規則,就會明白矩陣前乘和後乘得出來的結果是不一樣的,不過一般情況下都會選擇使用post方法,後乘。
    其中還有擴充套件方法比如:
  4. mapRect(rect) / mapRect(desRect,orgRect)
      第一個方法即使用原始矩陣代入運算,會將返回的矩陣直接覆蓋在傳入的矩陣中
      第二個方法則是對於需要儲存原始矩陣的情況下,會把原始矩陣的計算結果賦值到指定的矩陣中
  5. setRectToRect(src,des,stf)
      這個方法相當於將原始矩陣填充到目標矩陣中,所以也就要求兩個矩陣都是有值的。其中填充模式由第三個引數決定。
        /**
         * 獨立縮放X和Y,直到和src的rect和目標rect確切的匹配。這可能會改變原始rect的寬高比
         */
        FILL(0),
        /**
         * 在保持原有寬高比的情況下計算出一個合適的縮放比例,但也會確保原始rect合適的填入目標rect,
         * 最終會把開始的一個邊與目標的開始邊左邊對齊
         */
        START(1),
        /**
         * 與START類似,不過最終結果會盡可能居中
         */
        CENTER(2),
        /**
         * 與START類似,不過最終結果會盡可能靠右邊
         */
        END(3);
    複製程式碼
  6. invert(inverse)
      反轉矩陣,可以應用到類似倒影一類的實現中
  7. setPolyToPoly(src,srcIndex,dst,dstIndex,pointCount)   這是一個比較神奇的方法。隨著pointCount點數量,可以對原始矩陣進行平移、旋轉、錯切、翻頁效果。功能非常強大。

    此外,關於Matrix還有顏色變幻等效果,更多擴充套件用法後面會講到。

四、實踐到自定義view中

  寫一個自定義view,最重要的是要了解view的繪製過程。簡單的繪製流程如下

view繪製流程

其中不帶on的方法都為排程方法,不可被重寫,這些方法裡面會把前期一些必要的資料準備出來,帶on字首的方法都是實際進行處理的方法。
measure方法是測量控制元件大小的,layout是用來佈局,根據measure測量的結果,把其中每個元素在其內部進行位置的計算。最後會執行的draw方法,draw也分為draw和onDraw,可以根據自己需求來改寫對應的方法。
其中,onMeasure的方法如下所示:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // measure child img
        final int maxImgWidth = getMeasuredWidth();
        final int maxImgHeight = getMeasuredHeight();
        final int measureWidthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int measureHeightSize = MeasureSpec.getSize(heightMeasureSpec);
        int totalImageHeight = 0;
        // 縮放和旋轉影響size的交給measure
        for (int i = 0; i < imgList.size(); i++) {
            ImageData imageData = imgList.get(i);
            final int imgOrgWidth = imageData.getImgWidth();
            final int imgOrgHeight = imageData.getImgHeight();
            int imgRotateWidth;
            int imgRotateHeight;
            if (imageData.scale > 0) {
                imageData.matrix.setScale(imageData.scale, imageData.scale);
            } else {
                final float sizeProportion = (float) imgOrgWidth / imgOrgHeight;
                if (imgOrgHeight > imgOrgWidth) {
                    if (measureHeightSize == MeasureSpec.EXACTLY &&
                            imgOrgHeight > maxImgHeight) {
                        imgRotateWidth = (int) (maxImgHeight * sizeProportion);
                        imgRotateHeight = maxImgHeight;
                    } else {
                        imgRotateWidth = imgOrgWidth;
                        imgRotateHeight = imgOrgHeight;
                    }
                } else {
                    if (imgOrgWidth > maxImgWidth) {
                        imgRotateHeight = (int) (maxImgWidth / sizeProportion);
                        imgRotateWidth = maxImgWidth;
                    } else {
                        imgRotateWidth = imgOrgWidth;
                        imgRotateHeight = imgOrgHeight;
                    }
                }

                // resize
                imageData.reSize(imgRotateWidth, imgRotateHeight);
            }

            // rotate
            imageData.matrix.mapRect(imageData.drawRect, imageData.orgRect);
            imageData.matrix.postRotate(imageData.rotateAngle, imageData.drawRect.centerX(),
                    imageData.drawRect.top + (imageData.drawRect.height() * 0.5f));

            imageData.matrix.mapRect(imageData.drawRect, imageData.orgRect);
            totalImageHeight += imageData.drawRect.height();
        }
        switch (measureHeightSize) {
            // wrap_content
            case MeasureSpec.AT_MOST:
                setMeasuredDimension(MeasureSpec.makeMeasureSpec(maxImgWidth,
                        measureWidthSize), MeasureSpec.makeMeasureSpec(totalImageHeight, 
                        measureHeightSize));
                break;
            // match_parent or accurate num
            case MeasureSpec.EXACTLY:
                setMeasuredDimension(MeasureSpec.makeMeasureSpec(maxImgWidth,
				measureHeightSize));
                break;
            case MeasureSpec.UNSPECIFIED:
                setMeasuredDimension(MeasureSpec.makeMeasureSpec(maxImgWidth,
                        measureWidthSize), MeasureSpec.makeMeasureSpec(totalImageHeight, 
                        measureHeightSize));
                break;
        }
    }
複製程式碼

所有影響尺寸計算相關的方法都會放到這個measure裡面進行計算,比如scale和rotate,都會影響size大小。所以在這裡計算完成後,好在layout中進行正確的佈局。
layout中的程式碼如下:
   @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // measure child layout
        int cursorTop = top;
        int mid = (right - left) >> 1;
        for (int i = 0; i < imgList.size(); i++) {
            final ImageData imageData = imgList.get(i);

            // fix layout translate
            imageData.matrix.mapRect(imageData.drawRect, imageData.orgRect);
            int translateTop = (int) (cursorTop + (imageData.orgRect.top - 
            imageData.drawRect.top));
            int translateLeft = (int) (mid - imageData.drawRect.centerX());
            imageData.matrix.postTranslate(translateLeft, translateTop);

            imageData.matrix.mapRect(imageData.drawRect, imageData.orgRect);
            cursorTop = (int) imageData.drawRect.bottom;
        }
    }
複製程式碼

兩個方法中,要做到Matrix多效果疊加,切記要保留一個bitmap最原始的矩陣,然後再接下來的計算中需要用到當前尺寸的時候,使用Martix計算出臨時的尺寸對其進行計算。
兩個方法中,Bitmap被封裝到一個ImageData類裡面,進行物件化,這樣可以更好的管理Bitmap的處理和資料記錄。
ImageData如下:

  public class ImageData {
        public ImageData(Bitmap bitmap) {
            this.bitmap = bitmap;
            this.matrix = new Matrix();
            orgRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
        }

        // 預設置0
        float scale = 0f;
        // 0點在3點鐘方向,達到垂直居中的效果,需要置為-90度
        float rotateAngle = -90f;
        RectF drawRect = new RectF();
        RectF orgRect = new RectF();
        Bitmap bitmap;
        Matrix matrix;

        private float distanceStub = 0f;
        private float angleStub = 0f;

        public Bitmap getBitmap() {
            return bitmap;
        }

        public RectF getDrawRect() {
            return drawRect;
        }

        public int getImgWidth() {
            return bitmap.getWidth();
        }

        public int getImgHeight() {
            return bitmap.getHeight();
        }

        public void layout(int l, int t, int r, int b) {
            drawRect.set(l, t, r, b);
        }

        void reSize(int w, int h) {
            int orgWidth = bitmap.getWidth();
            int orgHeight = bitmap.getHeight();
            // 計算縮放比例
            float scaleWidth = ((float) w) / orgWidth;
            float scaleHeight = ((float) h) / orgHeight;
            scale = (scaleWidth + scaleHeight) * 0.5f;
            matrix.postScale(scale, scale);
        }

        void clearMatrixCache() {
            matrix.reset();
        }

        void setScale(float scale) {
            this.scale = scale;
        }

        float getScale() {
            return this.scale;
        }

        void setRotateAngle(float angle) {
            this.rotateAngle = angle;
        }

        float getRotateAngle() {
            return this.rotateAngle;
        }

        /**
         * imageData的觸控處理事件
         *
         * @param e 觸控事件
         */
        protected void onTouchEvent(MotionEvent e) {
	        // ...
        }

        private float getPointDistance(MotionEvent e) {
	        // ...
        }

        private float getPointAngle(MotionEvent e) {
	        // ...
        }
    }
複製程式碼

這裡面跟本文無關的方法都隱藏了,隨後會講到.
那麼我們來看看效果

實現效果


使用方法,跟目錄gradle裡面新增:
repositories {
			...
			maven { url 'https://jitpack.io' }
		}
複製程式碼

app.gradle中新增:
compile 'com.github.Kongdy:ImageStitching:v1.0.0'
複製程式碼

本文程式碼:github.com/Kongdy/Imag…
個人github地址:github.com/Kongdy
個人掘金主頁:juejin.im/user/595a64…
csdn主頁:blog.csdn.net/u014303003

相關文章