图片操作系列 —(2)手势旋转图片

前言

在上次的文章:图片操作系列 —(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 加入里面)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏haifeiWu与他朋友们的专栏

复杂业务下向Mysql导入30万条数据代码优化的踩坑记录

从毕业到现在第一次接触到超过30万条数据导入MySQL的场景(有点low),就是在顺丰公司接入我司EMM产品时需要将AD中的员工数据导入MySQL中,因此楼主负...

30040
来自专栏儿童编程

我不是算命先生,却对占卜有了疑惑——如何论证“占卜前提”的正确与否

事出有因,我对《周易》感兴趣了很多年。只是觉得特别有趣,断断续续学习了一些皮毛。这几天又偶然接触到了《梅花易数》,觉得很是精彩,将五行八卦天干地支都串联了起来。...

15410
来自专栏儿童编程

天干地支五行八卦的对应关系

19790
来自专栏Ken的杂谈

【系统设置】CentOS 修改机器名

18230
来自专栏儿童编程

什么样的人生才是有意义的人生——没有标准的标准答案

【导读】其实我们可以跳出这个小圈圈去更加科客观地看一下这个世界。在夜晚的时候我们仰望天空,浩瀚的宇宙中整个地球只是一粒浮尘,何况地球上一个小小的人类?在漫长的历...

1.8K50
来自专栏儿童编程

一张图理清《梅花易数》梗概

学《易经》的目的不一定是为了卜卦,但是了解卜卦绝对能够让你更好地了解易学。今天用一张思维导图对《梅花易数》的主要内容进行概括,希望能够给学友们提供帮助。

32440
来自专栏儿童编程

声音功能让儿童编程更有创造性

导读:Scratch中声音功能非常强大,除了常规的音效,你甚至可以模拟各种乐器的各个发音、设置节拍、休止……如果你愿意,甚至可以用它创作一个交响乐。我们可以引导...

13840
来自专栏儿童编程

《动物魔法学校》儿童学编程Scratch之“外观”部分

导读:本文通过一个案例《动物魔法学校》来学习Scratch语言的“外观”部分。之后通过一系列其他功能的综合运用对作品功能进行了扩展。

19240
来自专栏儿童编程

儿童创造力教育与编程教育的碰撞——MIT雷斯尼克教授最新理论梗概

儿童编程教育已经在我国各一线二线城市疯狂出现,颇有“烂大街”的趋势。我们不禁要问很多很多问题:

22370
来自专栏FSociety

SQL中GROUP BY用法示例

GROUP BY我们可以先从字面上来理解,GROUP表示分组,BY后面写字段名,就表示根据哪个字段进行分组,如果有用Excel比较多的话,GROUP BY比较类...

5.2K20

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励