1. 程式人生 > >Android 使用變形矩陣實現可以拖拽,縮放,旋轉的影象

Android 使用變形矩陣實現可以拖拽,縮放,旋轉的影象

上篇博文介紹了變形矩陣的一些用法,所以這篇博文就結合變形矩陣來實現一個可以拖拽、縮放、旋轉的影象吧。

首先,我們就繼承ImageView來實現我們的自定義View。

程式碼如下:

public class MyMatrixImg extends ImageView {

    private Context mContext;

    private float startX,startY;


    public MyMatrixImg(Context context, AttributeSet attrs) {
        super(context, attrs);
        this
.mContext = context; // 初始化 init(); } private void init() { /* * 獲取螢幕寬高 */ WindowManager manager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); DisplayMetrics outMetrics = new DisplayMetrics(); manager.getDefaultDisplay().getMetrics(outMetrics); int
Screenwidth = outMetrics.widthPixels; int Screenheight = outMetrics.heightPixels; /* * 設定圖片資源 */ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.hibiki); bitmap = Bitmap.createScaledBitmap(bitmap, Screenwidth, Screenheight, true
); setImageBitmap(bitmap); } }

這裡我們載入本地的一張圖片,並將其縮放成螢幕的大小。

拖拽的實現

拖拽,縮放,旋轉這三種操作中,最簡答的就是拖拽了。

在我們拖拽的時候只需要單點觸控即可完成操作,而縮放旋轉是需要多點觸控來實現的。關於多點觸控,後面再做詳細介紹。

現在就先從最簡答的入手。

單點觸控大家應該都很熟悉了:

在處理單點觸控中,我們一般會用到MotionEvent.ACTION_DOWNACTION_UPACTION_MOVE,然後可以用一個Switch語句來分別進行處理。ACTION_DOWNACTION_UP就是單點觸控式螢幕幕,按下去和放開的操作,ACTION_MOVE就是手指在螢幕上移動的操作。

下面就是隻判斷了單點觸控的實現拖拽的程式碼:

public class MyMatrixImg extends ImageView {

    private Context mContext;
    private Matrix currentMatrix, savedMatrix;// Matrix物件

    private float startX,startY;


    public MyMatrixImg(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;

        // 初始化
        init();
    }

    private void init() {
        /*
         * 例項化物件
         */
        currentMatrix = new Matrix();
        savedMatrix = new Matrix();

        /*
         * 獲取螢幕寬高
         */

        WindowManager manager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(outMetrics);
        int Screenwidth = outMetrics.widthPixels;
        int Screenheight = outMetrics.heightPixels;

        /*
         * 設定圖片資源
         */
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.hibiki);
        bitmap = Bitmap.createScaledBitmap(bitmap, Screenwidth, Screenheight, true);
        setImageBitmap(bitmap);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:// 單點接觸螢幕時
                savedMatrix.set(currentMatrix);
                startX=event.getX();
                startY=event.getY();

                break;
            case MotionEvent.ACTION_MOVE:// 觸控點移動時
                currentMatrix.set(savedMatrix);
                float dx = event.getX() - startX;
                float dy = event.getY() - startY;
                currentMatrix.postTranslate(dx, dy);
                break;

            case MotionEvent.ACTION_UP:// 單點離開螢幕時
                break;
        }

        setImageMatrix(currentMatrix);
        return true;
    }
}

xml如下:

<com.example.administrator.myview.MyMatrixImg
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="matrix"
        />

記得在xml中設定scaleType=”matrix”

這裡使用了currentMatrix, savedMatrix這兩個矩陣。分別表示現在的和儲存的。

當我們每次在螢幕落下手指時,savedMatrix就會獲取當前的矩陣的數值狀態。我們的初始矩陣都是一樣的也就是矩陣的對角線都是1。

當我們第一次手指觸控的時候,savedMatrix儲存的是初始矩陣。我們的圖片也在最初始的位置。這時記錄下手指落下的座標startX、startY。

當我們移動手指時,我們實時更新當前手指座標和初始座標之差dx、dy。然後就把這些差值賦值給currentMatrix的Translate()方法,進行平移。

每次移動的時候我們都會首先呼叫currentMatrix.set(savedMatrix);,是因為我們手指每次的移動的距離都應該根據初始位置做改變。所以這裡都會將currentMatrix賦為初值,之後再通過座標差來進行位移。

最後我們呼叫setImageMatrix(currentMatrix);來將矩陣的變化套用在影象上。

效果如下:

這裡寫圖片描述

縮放的實現

至於縮放操作,我們肯定是需要用到兩根手指了。這就涉及到Android的多點觸控。

理論上,Android系統本身可以處理多達256個手指的觸控,這主要取決於手機硬體的支援。當然,支援多點觸控的手機,也不會支援這麼多點,一般是支援2個點或者4個點。對於開發者來說,編寫多點觸控的程式碼與編寫單點觸控的程式碼,並沒有很大的差異。這是因為,Android SDK中的MotionEvent類不僅封裝了單點觸控的訊息,也封裝了多點觸控的訊息,對於單點觸控和多點觸控的處理方式幾乎是一樣的。

在處理多點觸控的過程中,我們還需要用到MotionEvent.ACTION_MASK。一般使用switch(event.getAction() & MotionEvent.ACTION_MASK)就可以處理處理多點觸控的ACTION_POINTER_DOWNACTION_POINTER_UP事件。程式碼呼叫這個“與”操作以後,當第二個手指按下或者放開,就會觸發ACTION_POINTER_DOWN或者ACTION_POINTER_UP事件。

在多點操作過程中,最先發生的是ACTION_DOWN,之後其他點的按下、擡起產生的動作為ACTION_POINTER_DOWNACTION_POINTER_UP,最後一個點擡起會產生ACTION_UP。對於ACTION_DOWNACTION_UP之間的其他點,Android稱之為maskedAction,可以使用函式public final int getActionMasked ()來查詢這個動作是ACTION_POINTER_DOWN還是ACTION_POINTER_UP,如果getActionMasked()返回了ACTION_MOVE,則表明當前使用者正在使用若干(一個或者多個)手指在螢幕上移動,沒有手指按下或擡起。函式public final int getActionIndex ()用來獲取當前按下/擡起的點的標識。如果當前沒有任何點擡起/按下,該函式返回0。

對於多點觸控的介紹就到這裡,下面我們在上一個demo的基礎上,增加縮放功能的實現。

public class MyMatrixImg extends ImageView {

    private Context mContext;
    private Matrix currentMatrix, savedMatrix;// Matrix物件

    private PointF startF= new PointF();
    private PointF midF;// 起點、中點物件

    // 初始的兩個手指按下的觸控點的距離
    private float oldDis = 1f;

    private static final int MODE_NONE = 0;// 預設的觸控模式
    private static final int MODE_DRAG = 1;// 拖拽模式
    private static final int MODE_ZOOM = 2;// 縮放模式
    private int mode = MODE_NONE;


    public MyMatrixImg(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;

        // 初始化
        init();
    }

    private void init() {
        /*
         * 例項化物件
         */
        currentMatrix = new Matrix();
        savedMatrix = new Matrix();

        /*
         * 獲取螢幕寬高
         */

        WindowManager manager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(outMetrics);
        int Screenwidth = outMetrics.widthPixels;
        int Screenheight = outMetrics.heightPixels;

        /*
         * 設定圖片資源
         */
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.hibiki);
        bitmap = Bitmap.createScaledBitmap(bitmap, Screenwidth, Screenheight, true);
        setImageBitmap(bitmap);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()& MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:// 單點接觸螢幕時
                savedMatrix.set(currentMatrix);
                startF.set(event.getX(), event.getY());
                mode=MODE_DRAG;
                break;

            case MotionEvent.ACTION_POINTER_DOWN:// 第二個手指按下事件
                oldDis = calDis(event);
                if (oldDis > 10F) {
                    savedMatrix.set(currentMatrix);
                    midF=calMidPoint(event);
                    mode = MODE_ZOOM;
                }

                break;
            case MotionEvent.ACTION_MOVE:// 觸控點移動時
                /*
                 * 單點觸控拖拽平移
                 */

                if (mode == MODE_DRAG) {
                    currentMatrix.set(savedMatrix);
                    float dx = event.getX() - startF.x;
                    float dy = event.getY() - startF.y;
                    currentMatrix.postTranslate(dx, dy);
                }
                /*
                 * 兩點觸控拖放
                 */
                else if(mode == MODE_ZOOM && event.getPointerCount() == 2){
                    float newDis = calDis(event);
                    currentMatrix.set(savedMatrix);

                    //指尖移動距離大於10F縮放
                    if (newDis > 10F) {
                        //通過先後兩次距離比計算出縮放的比例
                        float scale = newDis / oldDis;
                        currentMatrix.postScale(scale, scale, midF.x, midF.y);
                    }
                }

                break;
            case MotionEvent.ACTION_UP:// 單點離開螢幕時
            case MotionEvent.ACTION_POINTER_UP:// 第二個點離開螢幕時
                mode = MODE_NONE;
                break;


        }

        setImageMatrix(currentMatrix);
        return true;
    }

    // 計算兩個觸控點之間的距離
    private float calDis(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);
    }

    // 計算兩個觸控點的中點
    private PointF calMidPoint(MotionEvent event) {
        float x = event.getX(0) + event.getX(1);
        float y = event.getY(0) + event.getY(1);
        return new PointF(x / 2, y / 2);
    }
}

對於縮放的邏輯很簡單,主要就是通過兩根手指落點的初始距離和變化之後的距離之間的比例來計算出應該進行縮放的比例。

這裡我們使用了MODE_NONE等標識來標明當前是什麼操作。因為對於單點或者多點的操作,都會觸發ACTION_MOVE,所以我們需要用標識來判斷。

對於當我們雙指縮放過後擡起一根手指之後,我們應該繼續進行單指的拖動操作對吧?但是上面的程式碼卻是終止了各種操作。也就是說當我們擡起一根手指時,圖片時無法繼續拖動的。這樣顯然不符合我們的需求。所以對上述程式碼做如下改進:

case MotionEvent.ACTION_UP:// 單點離開螢幕時
case MotionEvent.ACTION_POINTER_UP:// 第二個點離開螢幕時
    mode = MODE_NONE;
    break;

改為:

case MotionEvent.ACTION_UP:// 單點離開螢幕時
    mode=MODE_NONE;
    break;
case MotionEvent.ACTION_POINTER_UP:// 第二個點離開螢幕時
    savedMatrix.set(currentMatrix);
    if(event.getActionIndex()==0)
        startF.set(event.getX(1), event.getY(1));
    else if(event.getActionIndex()==1)
        startF.set(event.getX(0), event.getY(0));
    mode=MODE_DRAG;
    break;

也就是說,當我們第二個手指離開的時候,先用event.getActionIndex()判斷是離開了那根手指,再獲取剩下的手指座標,將其設定為新的startF。這跟ACTION_DOWN時的處理邏輯相近。

這樣修改後,擡起手指後,剩下的那根手指也可以繼續進行拖動操作。

最終效果如下:

這裡寫圖片描述

旋轉的實現

知道縮放的實現思路之後,旋轉也就非常容易實現了 ,下面是程式碼:

public class MyMatrixImg extends ImageView {

    private Context mContext;
    private Matrix currentMatrix, savedMatrix;// Matrix物件

    private PointF startF= new PointF();
    private PointF midF;// 起點、中點物件

    // 初始的兩個手指按下的觸控點的距離
    private float oldDis = 1f;

    private float saveRotate = 0F;// 儲存了的角度值

    private static final int MODE_NONE = 0;// 預設的觸控模式
    private static final int MODE_DRAG = 1;// 拖拽模式
    private static final int MODE_ZOOM = 2;// 縮放模式
    private int mode = MODE_NONE;


    public MyMatrixImg(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;

        // 初始化
        init();
    }

    private void init() {
        /*
         * 例項化物件
         */
        currentMatrix = new Matrix();
        savedMatrix = new Matrix();

        /*
         * 獲取螢幕寬高
         */

        WindowManager manager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(outMetrics);
        int Screenwidth = outMetrics.widthPixels;
        int Screenheight = outMetrics.heightPixels;

        /*
         * 設定圖片資源
         */
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.hibiki);
        bitmap = Bitmap.createScaledBitmap(bitmap, Screenwidth, Screenheight, true);
        setImageBitmap(bitmap);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()& MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:// 單點接觸螢幕時
                savedMatrix.set(currentMatrix);
                startF.set(event.getX(), event.getY());
                mode=MODE_DRAG;
                break;

            case MotionEvent.ACTION_POINTER_DOWN:// 第二個手指按下事件
                oldDis = calDis(event);
                if (oldDis > 10F) {
                    savedMatrix.set(currentMatrix);
                    midF=calMidPoint(event);
                    mode = MODE_ZOOM;
                }
                saveRotate = calRotation(event);//計算初始的角度
                break;
            case MotionEvent.ACTION_MOVE:// 觸控點移動時

                /*
                 * 單點觸控拖拽平移
                 */

                if (mode == MODE_DRAG) {
                    currentMatrix.set(savedMatrix);
                    float dx = event.getX() - startF.x;
                    float dy = event.getY() - startF.y;
                    currentMatrix.postTranslate(dx, dy);
                }
                /*
                 * 兩點觸控拖放
                 */
                else if(mode == MODE_ZOOM && event.getPointerCount() == 2){
                    float newDis = calDis(event);
                    float rotate = calRotation(event);
                    currentMatrix.set(savedMatrix);

                    //指尖移動距離大於10F縮放
                    if (newDis > 10F) {
                        float scale = newDis / oldDis;
                        currentMatrix.postScale(scale, scale, midF.x, midF.y);
                    }

                    System.out.println("degree"+rotate);
                    //當旋轉的角度大於5F才進行旋轉
                    if(Math.abs(rotate - saveRotate)>5F){
                        currentMatrix.postRotate(rotate - saveRotate, getMeasuredWidth() / 2, getMeasuredHeight() / 2);
                    }
                }
                break;

            case MotionEvent.ACTION_UP:// 單點離開螢幕時
                mode=MODE_NONE;
                break;
            case MotionEvent.ACTION_POINTER_UP:// 第二個點離開螢幕時
                System.out.println(event.getActionIndex());
                savedMatrix.set(currentMatrix);
                if(event.getActionIndex()==0)
                    startF.set(event.getX(1), event.getY(1));
                else if(event.getActionIndex()==1)
                    startF.set(event.getX(0), event.getY(0));
                mode=MODE_DRAG;
                break;


        }

        setImageMatrix(currentMatrix);
        return true;
    }

    // 計算兩個觸控點之間的距離
    private float calDis(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);
    }

    // 計算兩個觸控點的中點
    private PointF calMidPoint(MotionEvent event) {
        float x = event.getX(0) + event.getX(1);
        float y = event.getY(0) + event.getY(1);
        return new PointF(x / 2, y / 2);
    }

    //計算角度
    private float calRotation(MotionEvent event) {
        double deltaX = (event.getX(0) - event.getX(1));
        double deltaY = (event.getY(0) - event.getY(1));
        double radius = Math.atan2(deltaY, deltaX);
        return (float) Math.toDegrees(radius);
    }
}

主要思路就是通過計算前後兩次的角度之差來決定旋轉的角度。這裡我判定當他們之差大於5才進行角度旋轉。

下面是效果:

這裡寫圖片描述

本篇文章參考 : 愛哥的部落格