Android 多点触控与图片缩放

上一章,我们学习了手势 GestureDecetor 的基本使用 Android 手势学习 GestureDetector,这一次,我们来学习使用 ScaleGestureDetector 来实现一个图片缩放的效果,如下:

文章参考鸿洋大大的图片缩放

scale.gif
代码 ScaleImageview

使用

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

然后在你的 module 中添加:

 implementation 'com.github.LillteZheng:ScaleImageview:1.0'

然后添加控件即可:

<com.zhengsr.mylibrary.ScaleImageView
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_margin="10dp"
    app:scale_limit_board="false"
    app:scale_max_factor="5"
    app:scale_auto_time="5"
    app:scale_double_factor="2"
    android:src="@mipmap/a1"/>

一、分析:

  1. 通过 ScaleGestureDetector 拿到缩放中心和缩放因子
  2. 通过 matrix 进行图片的缩放和平移
  3. 双击进行放大和缩小
  4. 结合 GestureDetector 进行平移
  5. 使用 getParent().requestDisallowInterceptTouchEvent(true); 进行事件发发截取

二、讲解

2.1 平移与缩放

首先,当加载一张图片的时候,并不清楚图片的大小,如果想要它居中且全部显示出来,则需要通过平移和缩放的形式,这里可以通过 Imageview 的 scaleTyle 设置成 matrix 的形式;比如,在初始化中:

setScaleType(ScaleType.MATRIX);

这样,则保证了图片的缩放模式是以 matrix 的。假设需要加载的图片比较小,在左上角,这时,需要移动到屏幕中心,如下图:
在这里插入图片描述
此时,需要将它先移动到屏幕中心,只需要屏幕的中心点减去图片的中心点座标即可,dw 和dh 为图片的宽高:

mMatrix = new Matrix();
float dx = width * 1.0f / 2 - dw * 1.0f / 2;
float dy = height * 1.0f / 2 - dh * 1.0f / 2;
mMatrix.postTranslate(dx,dy);

接着进行比例缩放,目前来说,有两种方式,假如整个屏幕只填放一张图片,那么,缩放只需要保证宽或者高一个比例缩放即可,只要保证了其中一个,以适配最小宽高来,那么另一个宽或者高也能适配,如下:

//控件宽高都比图片大,即放大了多少倍,拿到宽高的最小放大比例
 if (width > dw && height > dh){
     scale = Math.min((width * 1.0f / dw),(height * 1.0f / dh));
 }
 //图片宽高比控件大,即缩小了多少倍,,拿到宽高的最大放小比例
 if (dw > width && dh > height){
     scale = Math.max((width * 1.0f / dw),(height * 1.0f / dh));
 }
 //图片高比控件的高大,即应该缩放多少
 if (dw < width && dh > height){
     scale =  height * 1.0f / dh;
 }
 //图片宽比控件的宽大,即应该缩放多少
 if (dw > width && dh < height){
     scale = width * 1.0f / dw;
 }

这样拿到缩放比例后,直接用 matrix 即可:

mMatrix.postScale(scale,scale,width * 1.0f / 2, height * 1.0f / 2);

但如果你想效果图中,指定了宽高呢?这样如果只缩放其中一个宽或高,则另一个,就有可能存在不能完全适配的问题,所以,需要拿到宽和高的缩放比例,如:

scale = width * 1.0f / dw;
scaley = height * 1.0f / dh;

然后调用 matrix:

mMatrix.postScale(scale, scaley, width * 1.0f / 2, height * 1.0f / 2);

接着调用 imageview 自带的方法,即可完成移动中心点和缩放问题:

setImageMatrix(mMatrix);

可以使用

 app:scale_autofit="true"

来使用哪种比例,一个随便指定高度为200dp 的张方形图片不同效果如下:

scale_autofit=true,这样是宽高等比例缩放
scale_autofit=true,只以其中款或者高作为缩放
所以,如果你是指定高度,且图片是长方形的,建议 scale_autofit true,如果是 match_parent 且正方形的则 false 即可。

2.2 使用 ScaleGestureDetector 进行缩放

在进行平移缩放之后,可以使用 ScaleGestureDetector 进行缩放即可,它的三个方法如下:

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        //拿到缩放比例
        float scale = getScale();
        float scaleFactor = detector.getScaleFactor();
        //拿到缩放中心
        float focusX = detector.getFocusX();
        float focusY = detector.getFocusY();
        return true;
    }


    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }

需要注意的是 onScaleBegin return true,这样事件才能继续传递下去。接着在 onScale 拿到缩放比例中心和当前的缩放比例因子;整体代码如下:

 //拿到缩放比例
 float scale = getScale();
 float scaleFactor = detector.getScaleFactor();
 //拿到缩放中心
 float focusX = detector.getFocusX();
 float focusY = detector.getFocusY();
 //默认不能缩放至屏幕控件大小
 if (mIsLimitBroad) {
     //重新复制,让它逼近于最小值
     if (scale * scaleFactor < mDeflautScale) {
         scaleFactor = mDeflautScale / scale;
     }
     if (scale * scaleFactor > mMaxScale) {
         scaleFactor = mMaxScale / scale;
     }
 }
 mMatrix.postScale(scaleFactor,scaleFactor,focusX,focusY);
 checkBroad();
 setImageMatrix(mMatrix);

mIsLimitBroad 即 app:scale_limit_board 表示是否缩放能否能小于控件,如果为 true,则表示不能缩小到控件大小,可以通过比例换算去是实现,如果 scale * scaleFactor < mDeflautScale ,则让 scaleFactor = mDeflautScale / scale 即可。
其中 getScale()方法表示拿到当前的缩放比例:

 /**
  * 拿到缩放比例,默认拿到 x 即可
  * @return
  */
 private float getScale(){
     float[] floats = new float[9];
     mMatrix.getValues(floats);
     return floats[Matrix.MSCALE_X];
 }

checkBroad() 则是,在缩放时,如果不对中心点进行校正,不对图片进行平移校正,则会出现缩小时,图片跑到屏幕外侧或者中心点不对的问题,如下图这样:
在这里插入图片描述
所以,需要修正一下:

/**
     * 检查边界,让它不能有空白和让它一直保在屏幕中心
     */
    private void checkBroad() {
        RectF rectF = getMatrixRectF();
        float dx = 0;
        float dy = 0;
        //检测边界,不能让它留空白
        if (rectF.width() >= getWidth()) {
            if (rectF.right < getWidth()) {
                dx = getWidth() - rectF.right;
            }
            if (rectF.left > 0) {
                dx = -rectF.left;
            }
        }else {
            //保证在屏幕中心
            dx = getWidth() * 1.0f /2 - rectF.right + rectF.width() * 1.0f / 2;
        }
        //检测边界,不能让它留空白
        if (rectF.height() >= getHeight()){
            if (rectF.top > 0){
                dy = -rectF.top;
            }
            if (rectF.bottom < getHeight()){
                dy = getHeight() - rectF.bottom;
            }
        }else{
            //保证在屏幕中心
            dy = getHeight() * 1.0f /2 - rectF.bottom + rectF.height() * 1.0f / 2;
        }

        mMatrix.postTranslate(dx,dy);

    }

getMatrixRectF() 为拿到当前缩放后的图片大小:

    /**
     * 拿到缩放后的图片大小
     * @return
     */
    private RectF getMatrixRectF(){
        Matrix matrix = new Matrix(mMatrix);
        RectF rectF = new RectF();
        Drawable d = getDrawable();
        if (d != null){
            rectF.set(0,0,d.getIntrinsicWidth(),d.getIntrinsicHeight());
            matrix.mapRect(rectF);
        }
        return rectF;
    }

2.3 移动放大的图片

图片移动,在 onTouchEvent 中进行处理,需要考虑的是,当用两个手指拖动,然后其中一个手指擡起,此时中心点是有变动的,这时需要改变中心点的座标,不然会出现一下子跳动的现象,具体代码如下:

 //防止多指变动,中心点改变,而出现拖动问题
        int pointerCount = event.getPointerCount();
        float x = 0;
        float y = 0;
        for (int i = 0; i < pointerCount; i++) {
            x += event.getX(i);
            y += event.getY(i);
        }
        x = x / pointerCount;
        y = y / pointerCount;

        if (mLastPointCount != pointerCount){
            mLastx = x;
            mLasty = y;
        }
        //记录上一次的手指个数
        mLastPointCount = pointerCount;

        switch (event.getActionMasked()){
    
            case MotionEvent.ACTION_MOVE:
                requestFocusViewpager();
                float dx = x - mLastx;
                float dy = y - mLasty;
                mMatrix.postTranslate(dx,dy);
                checkBroad();
                setImageMatrix(mMatrix);
                mLastx = x;
                mLasty = y;
                break;
            case MotionEvent.ACTION_UP:
                mLastPointCount = 0;
                break;

            default:break;
        }

三、双击放大

双击的方法,我们也可以通过 GestureDetector 手势这个类拿到双击的方法:

    /**
     * 指监听 双击的方法
     */
    class SimpleGesture extends GestureDetector.SimpleOnGestureListener{

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            float x = e.getX();
            float y = e.getY();
            if (isCanDoubleScale) {
                //放大
                if (getScale() < mDoubleScale) {
                    post(new AutoScale(mDoubleScale, x, y));
                } else { //缩放到自身大小
                    post(new AutoScale(mDeflautScale, x, y));
                }
            }
            return true;

        }
    }

而AutoScale就是表示 缩放的类:

    /**
     * 双击缩放比例
     */
    class AutoScale implements Runnable {
        private final float LARGE = 1.07f;
        private final float SMALL = 0.93f;
        float targetScale;
        float x;
        float y;
        float tempScale;
        public AutoScale(float targetScale,float x, float y) {
            this.targetScale = targetScale;

            this.x = x;
            this.y = y;
            isCanDoubleScale = false;
            //放大
            if (targetScale > getScale()){
                tempScale = LARGE;
            }else{
                tempScale = SMALL;
            }
        }

        @Override
        public void run() {

            mMatrix.postScale(tempScale, tempScale , x, y);
            checkBroad();
            setImageMatrix(mMatrix);
            float currentScale = getScale();
            boolean isCanLarge = currentScale < targetScale && tempScale > 1.0f;
            boolean isCanSmall = currentScale > targetScale && tempScale < 1.0f;
            if (isCanLarge || isCanSmall) {
                //若还未到达缩放的目标值,则继续缩放
                postDelayed(this, mDoubleAutoTime);
            }else{
                //达到表示了,但可能有偏差,还需要调整一下
                mMatrix.postScale(targetScale/currentScale, targetScale/currentScale , x, y);
                checkBroad();
                setImageMatrix(mMatrix);
                isCanDoubleScale = true;
            }


        }
    }

这样,关键知识点就讲解完了,自定义属性如下:

缩放控件 ScaleImageView

名称 类型 说明
scale_auto_time reference,integer 双击时,达到放大的时间
scale_limit_board boolean 是否限制边界,即不能缩放到比控件小
scale_autofit boolean 自动适配缩放值,有些图片是正方形,如果你的高度没设定好,建议设置为false,不能会变形
scale_double_factor integer 双击时放大倍数
scale_max_factor integer 可放大的最大倍数
scale_interrupt_parent_touch boolean 是否截获父控件触摸事件,放大时,需要截取,不然无法移动
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章