圖片操作系列 —(2)手勢旋轉圖片

青蛙要fly發表於2017-11-27

前言

在上次的文章:圖片操作系列 —(1)手勢縮放圖片功能中,我們已經學會了如何用手勢來對圖片進行縮放。這次我們繼續來看第二個操作,那就是如何用手勢來旋轉圖片。

所以我們本文我們一共要實現二個功能:

  1. 根據二個手指頭的旋轉來使圖片跟著旋轉
  2. 當二個手指頭放開後,圖片會自動迴歸到合適的位置。

我說明下第二個功能點的意思:什麼叫回歸到合適的位置,比如如圖一,我們只轉動了一點點,沒有超過45度,然後放在手指,然後就會回到圖二的樣子。但是如果超過了45度,然後放開手指,就回變成圖三的樣子。

圖一
圖一

圖二
圖二

圖三
圖三


前面基本的東西說明我都不說了。比如Matrix等知識。大家可以直接參考圖片操作系列 —(1)手勢縮放圖片功能

ps:我這邊可以再貼出相關基礎的連結:
android matrix 最全方法詳解與進階(完整篇)
Android Matrix


根據二個手指頭的旋轉來使圖片跟著旋轉:

我們知道使圖片進行旋轉特定的角度很簡單:

使用Matrix.postRotate(float degrees, float px, float py)方法即可。繞著(px,py)點進行旋轉degrees角度。

所以我們的問題就變成了如果獲取二個手指頭在做旋轉手勢的時候,相應的角度的變化,從而通過Matrix.postRotate方法來讓圖片也跟著變化。

1.獲取二個手指頭的手勢監聽

圖片操作系列 —(1)手勢縮放圖片功能文中我們知道,控制圖片的縮放是專門有個ScaleGestureDetector;在OnTouch事件中把相應的事件傳遞給ScaleGestureDetector。然後監聽處理。我們也可以模仿著寫一個RotateGestureDetector來進行圖片旋轉的監聽和處理。

public interface IRotateDetector {

    /**
     * handle rotation in onTouchEvent
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    boolean onTouchEvent(MotionEvent event);

        /**
     * is the Gesture Rotate
     *
     * @return true:rotating;false,otherwise
     */
    boolean isRotating();
}複製程式碼
public class RotateGestureDetector implements IRotateDetector{

    private int mLastAngle = 0;//最後一次的角度值
    private IRotateListener mListener;//用來旋轉的回撥Listener
    private boolean mIsRotate;//是否處於旋轉

    //用來設定回撥Listener的方法
    public void setRotateListener(IRotateListener listener) {
        this.mListener = listener;
    }

    //用來接收觸控事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return doRotate(event);
    }

    //真正的計算手勢操作所得到的角度值的方法,及回撥呼叫。
    private boolean doRotate(MotionEvent ev) {
        if (ev.getPointerCount() != 2) {
            return false;
        }
        //Calculate the angle between the two fingers
        int pivotX = (int) (ev.getX(0) + ev.getX(1)) / 2;
        int pivotY = (int) (ev.getY(0) + ev.getY(1)) / 2;
        float deltaX = ev.getX(0) - ev.getX(1);
        float deltaY = ev.getY(0) - ev.getY(1);

        double radians = Math.atan(deltaY / deltaX);

        int degrees = (int) Math.round(Math.toDegrees(Math.atan2(deltaY,deltaX)));

        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mLastAngle = degrees;
                mIsRotate = false;
                break;
            case MotionEvent.ACTION_UP:
                mIsRotate = false;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                mLastAngle = degrees;
                mIsRotate = false;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_POINTER_UP:
                mIsRotate = false;
                upRotate(pivotX, pivotY);
                mLastAngle = degrees;
                break;
            case MotionEvent.ACTION_MOVE:
                mIsRotate = true;
                int degreesValue = degrees  - mLastAngle;
                if (degreesValue > 45) {
                    //Going CCW across the boundary
                    rotate(-5, pivotX, pivotY);
                } else if (degreesValue < -45) {
                    //Going CW across the boundary
                    rotate(5, pivotX, pivotY);
                } else {
                    //Normal rotation, rotate the difference
                    rotate(degreesValue, pivotX, pivotY);
                }
                //Save the current angle
                mLastAngle = degrees;
                break;
        }
        return true;
    }

    //回撥的方法之一:控制圖片根據手勢的變化實時進行旋轉
    private void rotate(int degree, int pivotX, int pivotY) {
        if (mListener != null) {
            mListener.rotate(degree, pivotX, pivotY);
        }
    }

    //回撥的方法之一:最後某個手指放開後,控制圖片自動迴歸到合適的位置。
    private void upRotate(int pivotX, int pivotY) {
        if (mListener != null) {
            mListener.upRotate(pivotX, pivotY);
        }
    }

}複製程式碼

2.獲取二個手指頭的角度變化

所以我們只需要來分析一下具體OnTouch事件中的doRotate方法即可:

//真正的計算手勢操作所得到的角度值的方法,及回撥呼叫。
private boolean doRotate(MotionEvent ev) {
    //如果觸控的手指頭不是2個,直接返回。
    if (ev.getPointerCount() != 2) {
        return false;
    }

    //獲取二個手指頭的中心點的X與Y值,等會選擇二個手指頭的中心點作為旋轉的中心
    int pivotX = (int) (ev.getX(0) + ev.getX(1)) / 2;
    int pivotY = (int) (ev.getY(0) + ev.getY(1)) / 2;
    //獲取二個手指頭之間的X和Y的差值
    float deltaX = ev.getX(0) - ev.getX(1);
    float deltaY = ev.getY(0) - ev.getY(1);
    //獲取角度
    int degrees = (int) Math.round(Math.toDegrees(Math.atan2(deltaY,deltaX)));

    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            mLastAngle = degrees;
            mIsRotate = false;
            break;
        case MotionEvent.ACTION_UP:
            mIsRotate = false;
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            mLastAngle = degrees;
            mIsRotate = false;
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_POINTER_UP:
            mIsRotate = false;
            upRotate(pivotX, pivotY);
            mLastAngle = degrees;
            break;
        case MotionEvent.ACTION_MOVE:
            mIsRotate = true;
            /*
            每次把上一次的角度賦值給mLastAngle,然後獲取當前新獲取的角度degrees,
            二者相減獲取到二個手指頭在移動的時候相應的角度變化。
            */
            int degreesValue = degrees  - mLastAngle;

            /*
            這裡主要出現這麼個情況,二個手指頭如果相隔有一段距離,那麼在移動的過程中,角度不會一下子變化很大.
            但是比如我們這裡故意二個手指頭是碰在一起的,然後二個手指頭稍微動一下,你就會發現角度變化會很大。
            這樣圖片就會瞬間也旋轉了很大的角度,讓人體驗感覺很怪,所以我們這裡瞬間順時針或者逆時針超過45度,都只移動5度值。
            */
            if (degreesValue > 45) {
                rotate(-5, pivotX, pivotY);
            } else if (degreesValue < -45) {
                rotate(5, pivotX, pivotY);
            } else {
                rotate(degreesValue, pivotX, pivotY);
            }
            //Save the current angle
            mLastAngle = degrees;
            break;
    }
    return true;
}複製程式碼

doRotate方法中最主要的就是根據二個手指頭觸控獲取到的X,Y的差值,根據Math.atan2來獲取到角度。我們具體來看下為什麼這樣可以來獲取角度:

先附上一個基礎概念:Math.atan與Math.atan2

假設我們先點選了(50,50),再點選(10,10),這時候我們的deltaX = 40,deltaY = 40;也就是說
我們的弧度就是Math.atan2(40,40),而角度就是再用Math.toDegrees對弧度進行轉換即可。最終獲得額角度是45度。

我們可以通過圖形來檢視為什麼Math.atan2(40,40)對應的角度是45度。

如果我們的第二個手指頭從(10,10)移動到了(50,10),也就是說最後變成了Math.atan2(40,0),根據圖形來看我們就知道是:

所以一共旋轉了45度,所以我們的圖片也跟著順時針旋轉45度即可。

那假如我們的二個手指頭的放入順序反過來,變成:

那這時候就變成了Math.atan2(-40,-40),我們根據圖形就知道了角度:

這時候還是跟剛才一樣的操作,把(10,10)這個點移動到了(50,10),那這時候就是Math.atan2(-40,0);

所以最終得到的旋轉的角度是(-135)-(-90) = 45度,所以最終也是順時針旋轉45度。所以我們不管是哪個手指頭先放下都不影響結果。

也許有人就會問了,你這邊按照二個手指的中點作為旋轉中心去旋轉,豈不是會旋轉超出原來的圖片的邊界。如果你還記得我們上一篇文章:圖片操作系列 —(1)手勢縮放圖片功能,這篇文章最後的內容講的就是當圖片超過邊界,如果能隨著手勢慢慢回到邊界裡面:checkMatrixBounds()

3.在Activity中設定Listener來進行圖片的旋轉

然後我們只需要在相應的Activity處對回撥回來的(degreesValue, pivotX, pivotY)三個值做相應的旋轉即可。

rotateGestureDetector.setRotateListener(new IRotateListener() {
    @Override
    public void rotate(int degree, int pivotX, int pivotY) {
        //圖片跟著手勢進行旋轉
        mSuppMatrix.postRotate(degree, pivotX, pivotY);
        //Post the rotation to the image
        checkAndDisplayMatrix();
    }

    @Override
    public void upRotate(int pivotX, int pivotY) {

        //當手指頭鬆開的時候,讓圖片自動更新到合適的位置。
        float[] v = new float[9];
        mSuppMatrix.getValues(v);
        // calculate the degree of rotation
        int angle = (int) Math.round(Math.toDegrees(Math.atan2(v[Matrix.MSKEW_Y], v[Matrix.MSCALE_X])));

        mRightAngleRunnable = new RightAngleRunnable(angle, pivotX, pivotY);
        photoView.post(mRightAngleRunnable);
    }
});複製程式碼

手指頭鬆開手圖片自動旋轉到合適位置:

我們知道,前面圖片跟著旋轉,是獲取到了(int degree, int pivotX, int pivotY)這三個值,然後讓mSuppMatrix.postRotate(degree, pivotX, pivotY);那我們就當手指頭鬆開的時候,獲取到最終這個圖片比原來變化了多少角度即可。然後根據這個當前最終圖片的變化角度來進行適當的旋轉,讓其旋轉到合適位置。

我們來具體看怎麼實現的:

@Override
public void upRotate(int pivotX, int pivotY) {

    //當手指頭鬆開的時候,讓圖片自動更新到合適的位置。
    float[] v = new float[9];
    mSuppMatrix.getValues(v);
    // calculate the degree of rotation
    int angle = (int) Math.round(Math.toDegrees(Math.atan2(v[Matrix.MSKEW_Y], v[Matrix.MSCALE_X])));

    mRightAngleRunnable = new RightAngleRunnable(angle, pivotX, pivotY);
    photoView.post(mRightAngleRunnable);
}複製程式碼

Matrix,中文裡叫矩陣,高等數學裡有介紹,在影象處理方面,主要是用於平面的縮放、平移、旋轉等操作。在Android裡面,Matrix由9個float值構成,是一個3*3的矩陣。最好記住。如下圖:


我們發現mSuppMatrix.getValues(v)方法返回的9個float值中,第一個為cosX,第四個為sinX,所以我們就取下標為0和3的值,也就是MSCALE_X和MSKEW_Y。我們用Math.atan2(v[Matrix.MSKEW_Y], v[Matrix.MSCALE_X])來獲取弧度。再用Math.toDegrees來獲取相應的最終圖片的旋轉的度數。

public class Matrix {

    public static final int MSCALE_X = 0;   //!< use with getValues/setValues
    public static final int MSKEW_X  = 1;   //!< use with getValues/setValues
    public static final int MTRANS_X = 2;   //!< use with getValues/setValues
    public static final int MSKEW_Y  = 3;   //!< use with getValues/setValues
    public static final int MSCALE_Y = 4;   //!< use with getValues/setValues
    public static final int MTRANS_Y = 5;   //!< use with getValues/setValues
    public static final int MPERSP_0 = 6;   //!< use with getValues/setValues
    public static final int MPERSP_1 = 7;   //!< use with getValues/setValues
    public static final int MPERSP_2 = 8;   //!< use with getValues/setValues

    ......
    ......
    ......
}複製程式碼

然後我們再把獲取到的角度和中心點,通過一個Runnable來進行圖片最後的矯正:

mRightAngleRunnable = new RightAngleRunnable(angle, pivotX, pivotY);
photoView.post(mRightAngleRunnable);複製程式碼

我們知道最後是RightAngleRunnable來進行圖片的矯正,所以我們具體來分析下這個Runnable:

class RightAngleRunnable implements Runnable {
        private static final int RECOVER_SPEED = 4;
        private int mOldDegree;
        private int mNeedToRotate;
        private int mRoPivotX;
        private int mRoPivotY;

        RightAngleRunnable(int degree, int pivotX, int pivotY) {
            Log.v("dyp4", "oldDegree:" + degree + "," + "calDegree:" + calDegree(degree));
            this.mOldDegree = degree;
            this.mNeedToRotate = calDegree(degree);
            this.mRoPivotX = pivotX;
            this.mRoPivotY = pivotY;
        }

        //最終計算需要矯正的角度值
        /*
            例如:

            比如最終是60度,這時候其實是超過了45度,應該矯正成90度,
            所以最終要多給它30度。順時針多選擇30度。這裡計算會得到30。

            比如如果是-60度,這時候應該是變成-90讀,所以我們逆時針多旋轉30度。
            這時候計算會得到-30。

            如果是20度,這時候沒有超過45度,所以應該矯正成0度,
            所以最終要逆時針轉回20度,所以這裡計算會得到-20。

            如果是-120度,這時候要變成-90度,所以要順時針轉回30度,
            所以計算會得到30。
        */
        private int calDegree(int oldDegree) {
            int N = Math.abs(oldDegree) / 45;
            if ((0 <= N && N < 1) || 2 <= N && N < 3) {
                return -oldDegree % 45;
            } else {
                if (oldDegree < 0) {
                    return -(45 + oldDegree % 45);
                } else {
                    return (45 - oldDegree % 45);
                }
            }
        }


        /*
        我們上面的calDegree方法可以獲得我們需要矯正的角度,但是我們不是一下子就讓圖片選擇N度,而是慢慢的轉過來。
        比如我們用RECOVER_SPEED = 4,4度的慢慢來旋轉過來,不會給使用者很突兀的感覺。
        */
        @Override
        public void run() {
            if (mNeedToRotate == 0) {
                return;
            }
            if (photoView == null) {
                return;
            }
            if (mNeedToRotate > 0) {
                //Clockwise rotation
                if (mNeedToRotate >= RECOVER_SPEED) {
                    mSuppMatrix.postRotate(RECOVER_SPEED, mRoPivotX, mRoPivotY);
                    mNeedToRotate -= RECOVER_SPEED;
                } else {
                    mSuppMatrix.postRotate(mNeedToRotate, mRoPivotX, mRoPivotY);
                    mNeedToRotate = 0;
                }
            } else if (mNeedToRotate < 0) {
                //Counterclockwise rotation
                if (mNeedToRotate <= -RECOVER_SPEED) {
                    mSuppMatrix.postRotate(-RECOVER_SPEED, mRoPivotX, mRoPivotY);
                    mNeedToRotate += RECOVER_SPEED;
                } else {
                    mSuppMatrix.postRotate(mNeedToRotate, mRoPivotX, mRoPivotY);
                    mNeedToRotate = 0;
                }
            }


            checkAndDisplayMatrix();
            Compat.postOnAnimation(photoView, this);
        }
    }複製程式碼

結尾

還是老樣子,希望大家不要吐槽。有問題留言哈哈。。O(∩_∩)O哈哈~
PS:有好的畫圖軟體介紹嗎。。求介紹o(╥﹏╥)o

附上Demo地址:ScaleImageVewDemo(已經把圖片旋轉的Activity demo 加入裡面)

相關文章