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才進行角度旋轉。

下面是效果:

這裏寫圖片描述

本篇文章參考 : 愛哥的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章