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才进行角度旋转。

下面是效果:

本篇文章参考 : 爱哥的博客

你可能感兴趣的:(Android学习笔记)