据说手势或双击缩放的ImageView,之多点触控放大

新年好!新的一年,新的征程!小伙伴们,继续奋斗...这两天看了一些关于"手势"的文章,想记录下学到的一些知识点,慢慢积累...大神可以绕道了!

[TOC]

准备开船中...扬帆起航ing...

概述

这是一个可以设置成圆角或者圆角矩形的ImageView,并且可以设置是否支持多点触控放大,缩小,旋转图片,双击放大缩小的自定义的控件。还有一个仿刮刮卡效果的自定义View。

具体实现案例:图片根据手势的开合进行放大与缩小,双击放大与缩小,以及放大后平移功能,具体看效果,见下图

图片 1效果图.gif

  • 想要进行图片的放大与缩小,起码要知道图片什么时间加载完毕吧,这里就要用到一个监听(OnGlobalLayoutListener:实现这个接口即可) 用来监听ImageView加载图片完毕 ;注意:此监听有的小伙伴可能在Activity的onCreate方法中为了获得控件的宽高用过,对了,就是它,来监听ViewTree的变化,但是使用时需要在onAttachedToWindow中注册监听,在onDetachedFromWindow中移除监听,具体实现看下面代码;
  • 图片缩放要以手指触控的中心点进行缩放,并且缩小时需要处理边界问题,必须保证图片居中显示;这里就需要用到Matrix这个类和ScaleGestureDetector这个类;下面先解释下Matrix这个类的使用方法:
    1. Matrix内部的值本质是个float类型的数组,为3*3的一维数组,具体的含义为:mScale_X mSkew_X mTrans_X 这三个值分别为:x轴缩放因子 x轴倾斜 x轴平移mSkew_Y mScale_Y mTrans_Y 这三个值分别为:y轴倾斜 y轴缩放因子 y轴平移MPERSP_0 MPERSP_1 MPERSP_2在具体使用时,其实我们没有必要构建这个float[9]的数组,使用Matrix提供的api即可进行平移缩放旋转等,具体方法为(postScale,postTranslate,postRotate等);注意:post后记得调用setImageMatrix(Matrix matrix)方法即可,具体实现看下面代码;
    2. ScaleGestureDetector这是类,是android用来专门处理多指触控的,里面有个OnScaleGestureListener内部接口,只需重写其两个参数的构造器的函数即可;OnScaleGestureListener这个接口具体实现有三个方法,切记在onScaleBegin中必须返回true,才会进入onScale()方法, 否则多指触控一直调用onScaleBegin方法 不会调用onScale和 onScaleEnd方法,具体的请看下面的代码;注意:想要把事件传递给多指触控,需要在onTouch方法中调用mScaleGestureDetector.onTouchEvent并返回true;具体请看最下面附属的完整代码;
 @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); //注册onGlobalLayoutListener getViewTreeObserver().addOnGlobalLayoutListener; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); //移除onGlobalLayoutListener getViewTreeObserver().removeGlobalOnLayoutListener; } /** * 捕获图片加载完成事件 onMeasure 和onDraw都不适合 */ @Override public void onGlobalLayout() { //初始化的操作 一次就好 为了保证对缩放只进行一次 if{ //得到控件的宽和高--不一定是屏幕的宽和高 可能会有actionBar等等 int width = getWidth() ; int height = getHeight(); //得到我们的图片 以及宽和高 Drawable drawable = getDrawable(); if(drawable == null){ return ; } /** * 这里说下Drawable这个抽象类,具体实现类为BitmapDrawable * BitmapDrawable这个类重写了getIntrinsicWidth()和getIntrinsicHeight()方法 * 这两个方法看字面意思就知道是什么了,就是得到图片固有的宽和高的 */ int intrinsicWidth = drawable.getIntrinsicWidth(); int intrinsicHeight = drawable.getIntrinsicHeight(); Log.e("SCALE_IMAGEVIEW", intrinsicWidth+":intrinsicWidth"); Log.e("SCALE_IMAGEVIEW", intrinsicHeight+":intrinsicHeight"); // 如果图片宽度比控件宽度小 高度比控件大 需要缩小 float scale = 1.0f ;//缩放的比例因子 if(width>intrinsicWidth && height<intrinsicHeight){ scale = height*1.0f/intrinsicHeight ; } // 如果图片比控件大 需要缩小 if(width<intrinsicWidth && height>intrinsicHeight){ scale = width*1.0f/intrinsicWidth ; } if((width<intrinsicWidth && height<intrinsicHeight) || (width>intrinsicWidth&&height>intrinsicHeight)){ scale = Math.min(width*1.0f/intrinsicWidth, height*1.0f/intrinsicHeight); } /** * 得到初始化缩放的比例 */ mInitScale = scale ; mMidScale = 2*mInitScale ;//双击放大的值 mMaxScale = 4*mInitScale ;//放大的最大值 //将图片移动到控件的中心 int dx = width/2 - intrinsicWidth/2 ; int dy = height/2 - intrinsicHeight/2 ; //将一些参数设置到图片或控件上 设置平移缩放 旋转 mMatrix.postTranslate; mMatrix.postScale(mInitScale, mInitScale, width/2, height/2);//以控件的中心进行缩放 setImageMatrix; mOnce = true ; } }

记录下从上面的代码中自己感觉的疑难点:

  1. Drawable是个抽象类,具体实现类为BitmapDrawable,这个类重写了getIntrinsicWidth()和getIntrinsicHeight()方法,这两个方法看字面意思就知道是什么了,就是得到图片固有的宽和高的
  2. ** 为了控制图片缩小时边界让图片实时居中显示,需要得到放大之后图片的宽高以及left top right bottom等值;因为我们已经有Matrix,使用Matrix,即可得到 ,请看如下代码**
 /** * 获得图片放大或缩小之后的宽和高 以及 left top right bottom的坐标点, * 通过rect.width rect.height rect.top rect.left rect.right rect.bottom 即可得到想要的值 * @return */ private RectF getMatrixRectF(){ Matrix matrix = mMatrix ; RectF rect = new RectF(); Drawable drawable = getDrawable(); if(null!=drawable){ rect.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight; matrix.mapRect; } return rect ; }
  • 既然要缩放,那就要知道本次在上次的基础上缩放的比例,因此需要首先知道图片已经缩放的比例;得到图片的缩放值后,就需要在ScaleGestureDetector的内部接口OnScaleGestureListener的onScale方法中处理缩放逻辑,具体实现请看下面代码:
 /** * 获取图片当前的缩放值 * @return */ public float getScale(){ float[] values = new float[9]; mMatrix.getValues; return values[Matrix.MSCALE_X]; } //缩放区间 initScale --- maxScale @Override public boolean onScale(ScaleGestureDetector detector) { float scale = getScale() ; //捕获用户多指触控时系统计算缩放的比例---因为有缩放区间,所以需要添加区间判断逻辑 float scaleFactor = detector.getScaleFactor(); Log.e("ScaleGestrueDetector", "scaleFactor:"+scaleFactor); if(getDrawable{ return true; } //最大最小控制 if((scale<mMaxScale&&scaleFactor>1.0f)||(scale>mInitScale&&scaleFactor<1.0f)){ if(scale*scaleFactor > mMaxScale){ scaleFactor = mMaxScale/scale ; } if(scale*scaleFactor < mInitScale){ scaleFactor = mInitScale/scale ; } mMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY; //不断检测 控制白边和中心位置 checkBorderAndCenterWhenScale(); setImageMatrix; } return true; }

注意:float scaleFactor = detector.getScaleFactor() 这个方法得到的是"用户多指触控时系统根据手势计算出缩放的比例因子,得到此缩放因子后,需要乘以图片现在的缩放比例,看是否在缩放区间;detector.getFocusX(), detector.getFocusY()得到多指触控的中心的x,y坐标,用来指定缩放的中心点"

  • 双击放大与缩小功能,需要重写GestureDetector类的两个参数的构造函数,第二个参数为OnGestureListener,具体实现类为SimpleOnGestureListener,只需要重写onDoubleTap()方法即可;注意:需要在onTouch()方法最上面通过此代码mGestureDetector.onTouchEvent传递给GestureDetector类进行双击控制,具体请看最先面附属的完整代码
 <com.serenity.view.ScaleImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="matrix" android:src="@drawable/scene1" />

由于自定义的ImageView使用了Matrix,需要在xml中配置scaleType,其实不配置也行,本人在自义定的ImageView的构造函数中调用了setScaleType(ScaleType.MATRIX)方法,不管在xml怎么配置都会在代码中将scaleType设置为matrix类型;

1.外部滑动方式与内部滑动方式不一样.2.外部滑动方式与内部滑动方式一致.3.上面两种情况的嵌套.

处理冲突的原则:a.对于上面的第一种情况:记录上次记录点减去当前点得到deltaX,deltaY

可以利用滑动路径和水平方向所形成的夹角来确定是那种滑动,如果小于45°,那自然就是横向,大于就是纵向.可以对比横向滑动距离和纵向滑动距离,那个大就是那个方向滑动距离大.

b.对于第二,三种情况

可以 根据业务写出处理规则, 比如当内部View滑动到顶部或者底部时响应外部View,我们就可以根据这个规则判断内部View有没有滑动到底, 如果有的话就不消费事件,没有的话就消费事件.具体怎么消费事件有两种方法.

效果展示

效果展示
录制的视频5.4M,可能打不开得下下来看。

1.外部拦截法

所有的事件都要经由decorView分发,所以我们可以在decorView处做文章如果父View需要事件,就拦截事件;否则就不拦截事件.具体实现在onInterceptTouchEvent()中处理.

public boolean onInterceptTouchEvent(MotionEvent event){ boolean interceptd = false; //获取当前动作所在点 int x =  event.getX(); int y =  event.getY(); switch(event.getAction{ case MotionEvent.ACTION_DOWN: //默认不拦截ACTION_DOWN,因为父View一旦拦截ACTION_DOWN,那么这个系列的事件都会交由它处理. interceptd = false; break; case MotionEvent.ACTION_MOVE: if(父容器需要当前点击事件){ interceptd = true; }else{ interceptd = false; } break; case MotionEvent.ACTION_UP: //默认不拦截ACTION_UP,因为子View如果响应当前系列事件没有ACTION_UP的话无法触发onClick()方法 interceptd = false; break; default: break; } //保存最后一个拦截点 mLastXIntercept = x; mLastYIntercept = y; return interceptd;}

相关知识点

  • android matrix 最全方法详解与进阶(完整篇)
    这里用于图形变换如:放大缩小,平移,旋转
  • Android的事件分发机制
    这里用于解决ImageView与ViewPager嵌套时的事件冲突
  • xFermode的原理及使用
    这里用于设置ImageView内图片的形状:圆形和圆角矩形
    还有自定义View的刮刮卡效果
  • GestureDetector与ScaleGestureDetector入门
    Google的扩展的手势监听api
  • 其他(来不及了,要开车了!默认你懂了)
2.内部拦截法

父容器默认不拦截任何事件,所有事件都交由子元素,子元素不需要再requestDisallowInterceptTouchEvent操控父元素处理,和上面的方法正好相反.

public boolean dispatchTouchEvent(MotionEvent event){ //获取当前点位置 int x =  event.getX(); int y =  event.getY(); switch(event.getAction{ case MotionEvent.ACTION_DOWN: /** *操控父元素不拦截ACTION_DOWN,因为ACTION_DOWN不受 ACTION_DISALLOW_INTERCEPT 标记控制, *所以一旦父元素拦截ACTION_DOWN,这个事件系列都会被交由父元素处理. */ parent.requestDisallowInterceptTouchEvent; break; case MotionEvent.ACTION_MOVE: int deltaX = X - mLastX; int deltaY = Y - mLastY; if(父容器需要此类事件){ //让父元素可以继续拦截MOVE事件 parent.requestDisallowInterceptTouchEvent; } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent;}

父元素要做出如下处理

public boolean onInterceptTouchEvent(MotionEvent event){ int action = event.getAction(); if(action == MotionEvent.ACTION_DOWN){ return false; }else{ return true; }}

默认拦截除了ACTION_DOWN以外的事件.这样子元素调用requestDisallowInterceptTouchEvent父元素才能继续拦截所需事件;

关键代码和注意事项

如有什么问题,敬请提出,十分感谢!希望越来越好,谢谢!

初始化

因为我们是使用matirx来做图形的变化,所以要设置 setScaleType(ScaleType.MATRIX);图形变换的方法setImageMatrix(Matrix matrix);才会生效。

接着我们在xml中配置ImageView

<com.example.administrator.imagetest.MyImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@mipmap/test" />

问题来了,图片太大显示不全,而且位于控件的左上角。我想让他像微信一样,自动调整到控件中心点,并且等比例缩放到一个屏幕放的下。这时我们就要在控件图像被绘制出来的时候,调整图片大小和位置

实现implements ScaleGestureDetector.OnScaleGestureListener接口

    /**
     * 控件被加载到窗口时,监听View变化
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            getViewTreeObserver().addOnGlobalLayoutListener(this);
        }
    }

    /**
     * 控件被销毁时,关闭监听
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
    }

初始化图片大小和位置:

    /**
     * 当View发生改变的时候,会调用这个监听,可能多次调用,所以要加判断
     */
    @Override
    public void onGlobalLayout() {
        if (once) {
            initView();
            once = false;
        }
    }

    /**
     * 初始化图片大小,位置
     */
    private void initView() {
        Drawable d = getDrawable();
        if (d == null)
            return;
        //获取imageview宽高
        int width = getWidth();
        int height = getHeight();

        //获取图片宽高
        startWidth = startWidth > 0 ? startWidth : d.getIntrinsicWidth();
        startHeight = startHeight > 0 ? startHeight : d.getIntrinsicHeight();

        float scale = 1.0f;

        //如果图片的宽或高大于屏幕,缩放至屏幕的宽或者高
        if (startWidth > width && startHeight <= height) {
            scale = (float) width / startWidth;
        }
        if (startHeight > height && startWidth <= width) {
            scale = (float) height / startHeight;
        }
        //如果图片宽高都大于屏幕,按比例缩小
        if (startWidth > width && startHeight > height) {
            scale = Math.min((float) startWidth / width, (float) startHeight / height);
        }
        //如果图片宽高都小于屏幕,选取差比较小的那个比例,放大
        if (startWidth < width && startHeight < height) {
            scale = Math.min((float) width / startWidth, (float) height / startHeight);
        }
        //将图片移动至屏幕中心
        matrix.postTranslate((width - startWidth) / 2, (height - startHeight) / 2);
        //然后再放大
        matrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
        firstScale = firstScale > 0 ? firstScale : scale;
        setImageMatrix(matrix);
        getMatrixRectF();
    }

这样图片就可以在整个控件中居中,并且完全显示了。

放大缩小

实现ScaleGestureDetector.OnScaleGestureListener接口,
在onScale(ScaleGestureDetector detector)中做放大缩小的逻辑处理。

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        //这个放大缩小是每次进行细微的变化,通过频繁变化,来改变图片大小
        //通过与imageView本身的宽高进行限制,最大不过4倍,最小不过四分之一
        //放大
        if (detector.getScaleFactor() >= 1) {
            if (Math.min(getMatrixRectF().bottom - getMatrixRectF().top, 
                    getMatrixRectF().right - getMatrixRectF().left) / getWidth() <= MAX_SCALE) {
                matrix.postScale(detector.getScaleFactor(),
                        detector.getScaleFactor(), getWidth() / 2, getHeight() / 2);
                setImageMatrix(matrix);
                getMatrixRectF();
            }
        } else {
            //缩小
            if (getWidth() / Math.min(getMatrixRectF().bottom - getMatrixRectF().top,
                    getMatrixRectF().right - getMatrixRectF().left) <= MIN_SCALE) {
                matrix.postScale(detector.getScaleFactor(), detector.getScaleFactor(), 
                        getWidth() / 2, getHeight() / 2);
                setImageMatrix(matrix);
                getMatrixRectF();
            }
        }
        return true;
    }

这时,我们发现放大的时候,图片超过了屏幕范围,却不能放大,这个体验超级差的。这个时候,就要实现GestureDetector.OnGestureListener,在onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)方法中处理滑动事件了:

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //判断宽度或者高度有一个大于控件宽高的
        if (((getMatrixRectF().right - getMatrixRectF().left) > getWidth()
                || (getMatrixRectF().bottom - getMatrixRectF().top) > getHeight())) {
            //滑动到横坐标边界,将事件交给viewpager处理
            if(amendment(-distanceX, -distanceY)[0]==0){
                //可以滑动,拦截Viewpager事件
                getParent().requestDisallowInterceptTouchEvent(false);
            }else{
                //可以滑动,拦截Viewpager事件
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            matrix.postTranslate(amendment(-distanceX, -distanceY)[0],
                    amendment(-distanceX, -distanceY)[1]);
        } else {
            getParent().requestDisallowInterceptTouchEvent(false);//不能滑动,viewpager处理事件
        }
        setImageMatrix(matrix);
        return false;
    }
    /**
     * 滑动前判断滑动是否会造成越界,并对最终滑动距离进行修正
     */
    private float[] amendment(float distanceX, float distanceY) {
        float[] dis = new float[]{distanceX, distanceY};
        //先判断图片的宽高是否大于屏幕,大于屏幕才能滑动
        if ((getMatrixRectF().bottom - getMatrixRectF().top) > getHeight()) {
            //判断Y轴上图片的顶部加上滑动的距离是否大于屏幕的顶部
            if (getMatrixRectF().top + dis[1] > getTop()) {
                //如果超过了,重新设置滑动距离,使之不超过边界
                dis[1] = getTop() - getMatrixRectF().top;
            }
            if (getMatrixRectF().bottom + dis[1] < getBottom()) {
                dis[1] = getBottom() - getMatrixRectF().bottom;
            }
        } else {
            dis[1] = 0;
        }
        if ((getMatrixRectF().right - getMatrixRectF().left) > getWidth()) {
            if (getMatrixRectF().left + dis[0] > getLeft()) {
                dis[0] = getLeft() - getMatrixRectF().left;
            }
            if (getMatrixRectF().right + dis[0] < getRight()) {
                dis[0] = getRight() - getMatrixRectF().right;
            }
        } else {
            dis[0] = 0;
        }
        return dis;
    }
    /**
     * 根据当前图片的Matrix获得图片的范围
     * @return
     */
    private RectF getMatrixRectF() {
        Drawable d = getDrawable();
        if (null != d) {
            rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
            matrix.mapRect(rect);
        }
        return rect;
    }

这里的代码比较多,可实际上滑动图片也就是matrix.postTranslate();这个方法而已。只要是还要做一个图片是否可以滑动的判断,是否滑动越界,还有处理嵌套ViewPager时的事件分发冲突处理。

双击放大缩小

这里我们不能设置成第一次双击放大,第二次双击缩小,这样子的。正常我们是会设置最大倍数和最小倍数的,如果用户先用触控把图片拉到最大,然后双击图片。这个时候,图片就没有反应了。所以要根据当前的一个放大倍率来决定是要放大还是缩小图片。

    /**
     * 双击放大缩小
     */
    private void doubleClick() {
        gestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                return false;
            }

            @Override
            public boolean onDoubleTap(MotionEvent e) {
                scaleType();
                return false;
            }

            @Override
            public boolean onDoubleTapEvent(MotionEvent e) {
                return false;
            }
        });
    }
    /**
     * 双击放大缩小的动画
     */
    private void scaleType() {
        final float scale = getScale() / firstScale;
        //想要放大缩小的中心是在屏幕的中心,那么图片的中心也要在屏幕中心,
        // 这是就得通过计算偏移量,把图片先移动到屏幕中心来
        if (scale >= 1 && scale <= 2) {
            matrix.postTranslate(getWidth() / 2 - getMatrixRectF().centerX(), 
                    getHeight() / 2 - getMatrixRectF().centerY());
            matrix.postScale(MAX_SCALE / scale, MAX_SCALE / scale,
                    getWidth() / 2, getHeight() / 2);
            setImageMatrix(matrix);
            getMatrixRectF();
        } else {
            matrix.postTranslate(getWidth() / 2 - getMatrixRectF().centerX(),
                    getHeight() / 2 - getMatrixRectF().centerY());
            matrix.postScale(1 / scale, 1 / scale, getWidth() / 2, getHeight() / 2);
            //将图片移动至屏幕中心
            setImageMatrix(matrix);
            getMatrixRectF();
        }
    }

注意啦,敲黑板!

这段处理双击逻辑的代码,在缩放之前添加了一个平移的方法,这是因为如果我们在放大以后,移动图片再双击缩小的话,图片的位置会改变,不再是屏幕的中心(原来的位置是在屏幕的中心)而我们在缩放的时候是以屏幕的中心位置为缩放中心进行缩放的,所以图片的位置会发生偏移。但是我们通过触控放大缩小图片的时候不会出现这种情况,是因为move事件的频率快,每次变化只有一点点,这个时候平移和缩放是在交替执行的,所以能够自动修正到正确的位置

快十二点了,剩下的周五再写吧。下方有这个demo的源码地址(效率好低)

项目地址链接

github项目地址

个人公众号

图片 2

不只是技术文章哦,快关注我吧。
搜索公众号:kedasay

本文由华夏彩票发布于编程应用,转载请注明出处:据说手势或双击缩放的ImageView,之多点触控放大

您可能还会对下面的文章感兴趣: