基于ImageView的多点触控,双击放大缩小以及结合ViewPager的事件冲突

来源:互联网 发布:海盐行知中学 编辑:程序博客网 时间:2024/06/04 17:52
备注说明:通过慕课网中的“打造个性的图片预览与多点触控”视频的学习,特意将实现的思路给理出来、记录下来,作为自己的学习总结。文中存在不对之处,请大家踊跃指出,转载请标明出处。

本文通过一个自定义控件ZoomImageView,实现多点触控(移动,多点缩放),双击放大缩小,并在放大缩小过程自解决边界白边的情况,同时结合ViewPager的使用,并解决存在的事件冲突情况。
效果展示:
这里写图片描述

一、实现图片的居中显示(图片小时,进行放大;图片大时,进行缩小,并将图片置于居中显示),并实现多点缩放。

1、获得图片的宽高以及控件的宽高
(1)通过getDrawable()得到控件的图片,通过Drawable对象中getIntrinsicWidth()、getIntrinsicHeight()得到图片的宽高。
(2)通过实现ViewTreeObserver.onGlobalLayoutListener监听器,该布局监听器会在控件生成布局之后,会回调其中的onGlobalLayout()方法,在该方法中得到控件的宽高。但是在获取控件宽高之前需要为onGlobalLayoutListener注册、移除监听。

@Override    protected void onAttachedToWindow() {//当View,Attach到window中会执行这方法,需要注册onGlobalLayoutListener监听        super.onAttachedToWindow();getViewTreeObserver().addOnGlobalLayoutListener(this);//注册该监听    }    @SuppressWarnings("deprecation")    @Override    protected void onDetachedFromWindow() {//当View从window中移除时会执行这方法,需要移除onGlobalLayoutListener监听        super.onDetachedFromWindow();getViewTreeObserver().removeGlobalOnLayoutListener(this);//移除该监听    }

(3)得到控件的宽高以及图片的宽高时,比较他们的宽高情况,进行缩放处理,同时根据他们的中心点,进行平移操作。
具体代码如下:

    @Override    public void onGlobalLayout() {        // TODO Auto-generated method stub        if(!mOnce){//在控件加载图片过程中会多次调用这个方法        int width=getWidth();//得到控件的宽度        int height=getHeight();//得到控件高度        Drawable d=getDrawable();        if(d==null)            return;        int dw=d.getIntrinsicWidth();//得到图片的宽度        int dh=d.getIntrinsicHeight();//得到图片的高度        float scale=1.0f;//缩放值        if(dw>width&&dh<height){//图片宽度超出控件的宽度,需要缩小处理            scale=width*1.0f/dw;//以宽度缩放为主        }        if(dw<width&&dh>height){//图片高度超出控件的高度,            scale=height*1.0f/dh;//以高度进行缩放        }        if(dw>width&&dh>height){//图片宽高都大于控件的宽高时            scale=Math.min(width*1.0f/dw, height*1.0f/dh);//以其中较小的缩小值为主        }        if(dw<width&&dh<height){//图片宽高都小于控件的宽高            scale=Math.min(width*1.0f/dw, height*1.0f/dh);//以其中一个较小的放大值为主        }        mInitScale = scale;//缩小时,最小缩放量        mMidScale=mInitScale*2;//双击时的缩放量        mMaxScale = mInitScale * 4;//放大时,最大的放大量        //平移中心点        float dx=width*1.0f/2-dw*1.0f/2;        float dy=height*1.0f/2-dh*1.0f/2;        mScaleMatrix.postTranslate(dx, dy);        mScaleMatrix.postScale(mInitScale,mInitScale, width*1.0f/2, height*1.0f/2);        setImageMatrix(mScaleMatrix);        mOnce=true;        }    }

图片居中显示
2、实现多点触控时的缩放功能。
(1)在Android的SDK中已提供多点触控缩放实现ScaleGestureDetector,通过该类可以较为方便地实现多点缩放。创建这个对象时,需要传入一个缩放手指监听OnScaleGestureListener,此外只需要将触摸事件传给ScaleGestureDetector的触摸事件即可。

private ScaleGestureDetector mScaleGestureDetector;// 多点触控时的缩放操作mScaleGestureDetector = new ScaleGestureDetector(context, this);setOnTouchListener(this);....@Override    public boolean onTouch(View arg0, MotionEvent event) {        // TODO Auto-generated method stub        mScaleGestureDetector.onTouchEvent(event);        return true;    }

(2)在实现OnScaleGestureListener缩放手势监听时,需要复写onScale(缩放中)、onScaleBegin(缩放开始)、onScaleEnd(缩放结束)方法,其中onScaleBegin只需要返回值返回true即可,表示保证事件能够继续传递;具体缩放代码在onScale中实现。在缩放过程中需要为了避免无限放大或缩小,所以需要在缩放的时候获取当前缩放的值,通过计算最终缩放的值与最大的放大值和最小的缩放值进行对比。使缩放量在mInitScale~mMaxScale之间。

@Override    public boolean onScale(ScaleGestureDetector dector) {        float x = dector.getFocusX();//当前手指的中心点        float y = dector.getFocusY();        float scaleFactor = dector.getScaleFactor();// 当前手势操作准备的缩放的值        if(scaleFactor>1.0f){//当前想放大            if(scaleFactor*getScale()>mMaxScale){//当超过最大缩放量时,                scaleFactor=mMaxScale/getScale();            }        }else{//缩小            if(scaleFactor*getScale()<mInitScale){//小于最小的缩放量时                scaleFactor=mInitScale/getScale();            }        }        mScaleMatrix.postScale(scaleFactor, scaleFactor, x, y);        setImageMatrix(mScaleMatrix);        return true;//这里需要返回true,保证事件能够继续执行    }    /**得到当前的缩放值*/    private float getScale(){        float[] values=new float[9];        mScaleMatrix.getValues(values);        return values[Matrix.MSCALE_X];    }    @Override    public boolean onScaleBegin(ScaleGestureDetector detector) {// 缩放开始        return true;// 这里需要返回true,保证事件能够继续执行    }        /**得到当前的缩放值*/    private float getScale(){        float[] values=new float[9];        mScaleMatrix.getValues(values);        return values[Matrix.MSCALE_X];    }

这里写图片描述
这里写图片描述
(3)在缩放过程中发现如上图所示会出现边界白边的现象,为了在缩放的时候不出现边界白边的情况,那么就需要在缩放的时候图片进行相应的移动。具体思路:1.获取图片缩放后的宽高。2,根据图片放大后的宽高,与控件的宽高进行比较,得到水平方向和垂直方向的平移偏移量。
1.得到图片缩放后的宽高。

    /**     *得到图片放大以后的宽和高     */    private RectF getMatrixRectf(){        RectF rectF=new RectF();        Matrix matrix=mScaleMatrix;        Drawable d=getDrawable();        if(d!=null){            rectF.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());//得到放大缩小以后的图片宽和高,left ,right,top, bottom            matrix.mapRect(rectF);        }        return rectF;    }
2.获得平移所需要的偏移量。
/**     *在缩放的时候进行边界控制,并居中,防止缩放的时候出现白边     */    private void checkBorderAndCenterWhenScale(){        RectF rectF=getMatrixRectf();        float dx=0;//水平方向需要调整的偏移量        float dy=0;//垂直方向需要调整的偏移量        int width=getWidth();//控件宽度        int height=getHeight();//控件高度        if(rectF.width()>width){//放大图片大于控件宽度            if(rectF.left>0){//右边偏移,需要向左移动                dx=-rectF.left;            }            if(rectF.right<width){//左边偏移,需要向右移动                dx=width-rectF.right;            }        }        if(rectF.height()>height){//放大图片大于控高度            if(rectF.top>0){//向下偏离,需要向上移动                dy=-rectF.top;            }            if(rectF.bottom<height){//向上偏移,需要向下移动                dy=height-rectF.bottom;            }        }        if(rectF.width()<width||rectF.height()<height){//如果宽高小于控件的宽高时,需要居中显示            dx=width*1.0f/2-rectF.width()/2-rectF.left;            dy=height*1.0f/2-rectF.height()/2-rectF.top;        }        mScaleMatrix.postTranslate(dx, dy);    } 

二、实现图片的自由移动。

1、多点触摸时,图片的自由移动实现。具体思路:获取触摸的手指数量,得到每一触摸点的坐标,计算它们的中心点的坐标;根据上次中心点与这次触摸的中心点坐标的偏移,得到移动的偏移量, 此外还需要根据偏移量与系统默认设置的最小移动偏移量进行比较来判断是否符合响应移动的动作。
(1)比较是否符合响应移动的条件。

mTouchSlop=ViewConfiguration.get(context).getScaledTouchSlop();//获得系统设置的移动偏移量/**判断是否符合移动的条件*/    private boolean isMoveAction(float dx,float dy){        return Math.sqrt(dx*dx+dy*dy)>=mTouchSlop;    }

(2)根据中心点偏移量移动图片。

        @Override    public boolean onTouch(View arg0, MotionEvent event) {        // TODO Auto-generated method stub        mScaleGestureDetector.onTouchEvent(event);        float x=0;//触摸的中心点位置        float y=0;        int curPointCount=event.getPointerCount();        for(int i=0;i<curPointCount;i++){            x+=event.getX(i);            y+=event.getY(i);        }        x/=curPointCount;//计算得到多点触摸的中心点        y/=curPointCount;        if(mLastPointCount!=curPointCount){//当手指个数发生变化时,重新设置上次的中心点            mLatX=x;            mLatY=y;        }        mLastPointCount=curPointCount;        switch (event.getAction()) {        case MotionEvent.ACTION_MOVE:            float dx=x-mLatX;            float dy=y-mLatY;            if(!isCanDrag){                isCanDrag=isMoveAction(dx, dy);            }            if(isCanDrag){                if(getDrawable()!=null){                    RectF rectF=getMatrixRectf();                    if(rectF.width()<=getWidth()){//当缩放后的宽度小于控件宽度时,不让水平移动                        dx=0;                    }                    if(rectF.height()<=getHeight()){                        dy=0;                    }                    mScaleMatrix.postTranslate(dx, dy);                    setImageMatrix(mScaleMatrix);                    mLatX=x;                    mLatY=y;                    }            }            break;        case MotionEvent.ACTION_CANCEL:        case MotionEvent.ACTION_UP:            mLastPointCount=0;            break;        }        return true;    }

2.在自由移动过程中,会出现白边的问题,需要在移动的时候不断地进行边界的处理。

    /**     *在移动的时候进行边界控制,并居中,防止移动的时候出现白边     */    private void checkBorderAndCenterWhenTrans(){        RectF rectF=getMatrixRectf();        float dx=0;//水平方向需要调整的偏移量        float dy=0;//垂直方向需要调整的偏移量        int width=getWidth();//控件宽度        int height=getHeight();//控件高度        if(rectF.left>0&&isCheckLeftAndRight){            dx=-rectF.left;        }        if(rectF.right<width&&isCheckLeftAndRight){            dx=width-rectF.right;        }        if(rectF.top>0&&isCheckTopAndBottom){            dy=-rectF.top;        }        if(rectF.bottom<height&&isCheckTopAndBottom){        dy=height-rectF.bottom;        }        mScaleMatrix.postTranslate(dx, dy);    } 

其中 isCheckLeftAndRight、isCheckTopAndBottom,分别表示是否水平方向,垂直方向是否检测,因为当图片居中显示时,在垂直方向上出现空白情况,不算是白边问题,不应该让边界检测移动。
最终代码如下:

@Override    public boolean onTouch(View arg0, MotionEvent event) {        // TODO Auto-generated method stub        mScaleGestureDetector.onTouchEvent(event);        float x=0;//触摸的中心点位置        float y=0;        int curPointCount=event.getPointerCount();        for(int i=0;i<curPointCount;i++){            x+=event.getX(i);            y+=event.getY(i);        }        x/=curPointCount;//计算得到多点触摸的中心点        y/=curPointCount;        if(mLastPointCount!=curPointCount){//当手指个数发生变化时,重新设置上次的中心点            mLatX=x;            mLatY=y;        }        mLastPointCount=curPointCount;        switch (event.getAction()) {        case MotionEvent.ACTION_MOVE:            float dx=x-mLatX;            float dy=y-mLatY;            if(!isCanDrag){                isCanDrag=isMoveAction(dx, dy);            }            isCheckLeftAndRight=true;            isCheckTopAndBottom=true;            if(isCanDrag){                if(getDrawable()!=null){                    RectF rectF=getMatrixRectf();                    if(rectF.width()<=getWidth()){//当缩放后的宽度小于控件宽度时,不让水平移动                        dx=0;                        isCheckLeftAndRight=false;                    }                    if(rectF.height()<=getHeight()){                        dy=0;                        isCheckTopAndBottom=false;                    }                    mScaleMatrix.postTranslate(dx, dy);                    checkBorderAndCenterWhenTrans();                    setImageMatrix(mScaleMatrix);                    mLatX=x;                    mLatY=y;                    }            }            break;        case MotionEvent.ACTION_CANCEL:        case MotionEvent.ACTION_UP:            mLastPointCount=0;            break;        }        return true;    }

三、实现双击放大缩小功能。

1、双击放大、缩小的实现,在Android SDK中提供GestureDetector类有很多关于手势的操作,也包含双击事件的响应,虽然我可以实现GestureDectorListener监听器,但是需要重写的方法比较多,我们可以实现GestureDetector.SimpleOnGestureListener监听,只要重写一个onDoubleTapEvent方法即可实现双击的事件。
需要特别注意的是:GestureDetector也需要在onTouch给她传递触摸事件。
mGestureDetector.onTouchEvent(event);

        mGestureDetector=new GestureDetector(context, new GestureDetector.SimpleOnGestureListener(){            @Override            public boolean onDoubleTap(MotionEvent e) {                // TODO Auto-generated method stub                float scale=1.0f;                if(getScale()<mMidScale&&getScale()>=mInitScale){//双击放大至mMidScale                    scale=mMidScale/getScale()+0.01f;                }else if(getScale()<mMaxScale&&getScale()>mMidScale){//双击继续放大至mMaxScale                    scale=mMaxScale/getScale();                }else if(getScale()==mMaxScale){//已经是最大时,此时双击缩小至mMidScale                    scale=mMidScale/getScale();                }else if(getScale()==mMidScale){//双击继续缩小至mInitScale缩小到最小                    scale=mInitScale/getScale();                }                mScaleMatrix.postScale(scale, scale, e.getX(), e.getY());                checkBorderAndCenterWhenScale();                setImageMatrix(mScaleMatrix);                return true;            }        });

2、实现缓慢的缩放效果,具体思路:使用Runnable创建一个线程用于慢慢地进行放大(缩小),在每次缩放后与目标的缩放效果进行比较,指导达到所需要的缩放值。

/**缓慢缩放实现*/public class AutoScaleRunnable implements Runnable{    private float mTagScale;//最终缩放效果值    private float BIGING=1.07f;//每次放大值    private float SMALE=0.93f;//每次缩小值    private float mTempScale;//当前缩放值    private float x;    private float y;    public AutoScaleRunnable(float mTagScale,float x,float y){        this.mTagScale=mTagScale;        this.x=x;        this.y=y;        if(getScale()<mTagScale){            mTempScale=BIGING;        }        if(getScale()>mTagScale){            mTempScale=SMALE;        }    }    @Override    public void run() {        mScaleMatrix.postScale(mTempScale, mTempScale, x, y);        checkBorderAndCenterWhenScale();        setImageMatrix(mScaleMatrix);        if((mTempScale==BIGING&&getScale()<mTagScale)||(mTempScale==SMALE&&getScale()>mTagScale)){            postDelayed(this, 16);        }else{//达到目标的缩放值            mScaleMatrix.postScale(mTagScale/getScale(), mTagScale/getScale(), x, y);            checkBorderAndCenterWhenScale();            setImageMatrix(mScaleMatrix);        }       }   }

此时双击缩放方法中具体实现如代码所示:

mGestureDetector=new GestureDetector(context, new GestureDetector.SimpleOnGestureListener(){            @Override            public boolean onDoubleTap(MotionEvent e) {                // TODO Auto-generated method stub                float scale=1.0f;                float x=e.getX();                float y=e.getY();                if(getScale()<mMidScale&&getScale()>=mInitScale){//双击放大至mMidScale                    scale=mMidScale+0.01f;                }else if(getScale()<mMaxScale&&getScale()>mMidScale){//双击继续放大至mMaxScale                    scale=mMaxScale;                }else if(getScale()==mMaxScale){//已经是最大时,此时双击缩小至mMidScale                    scale=mMidScale;                }else if(getScale()==mMidScale){//双击继续缩小至mInitScale缩小到最小                    scale=mInitScale;                }                postDelayed(new AutoScaleRunnable(scale, x, y), 16);                return true;            }        });

四、结合ViewPager使用解决事件冲突
1、与ViewPager事件冲突原因是在当图片的宽度或高度超过控件时,按照正常逻辑此时需要控件能够水平、垂直方向的平移操作,但是与ViewPager的水平移动操作事件存在冲突。
在View事件分发系统中,事件最开始有父布局中的子控件获得事件,当子控件响应事件操作时,才会传给父控件;然后如果父控件将该事件拦截时,那么子控件就无法获取。此时子控件如果需要父控件不被拦截,只需要通知父控件不拦截该事件。getParent().requestDisallowInterceptTouchEvent(true);

if((rectF.height()>getHeight())||(rectF.width()>getWidth())){                getParent().requestDisallowInterceptTouchEvent(true);            }

五、总结

  1. 添加OnGlobalLayoutListener 监听,得到控件宽高,在布局为控件分配宽高后,会回调这个监听
  2. 在onAttachedToWindow中添加GlobalLayout监听 getViewTreeObserver().addOnGlobalLayoutListener(this);
  3. 在onDetachedFromWindow中移除GlobalLayout监听 getViewTreeObserver().removeOnGlobalLayoutListener(this);
  4. 添加OnScaleGestureListener监听,实现多点触控,实现图片多点缩放,需要实现OnTouchListener事件
  5. .在onScale(正在缩放处理)中,添加缩放的处理
  6. 在缩放的处理中,添加对边界白边的处理checkBorderAndCenterWhenScale,不断将图片至于居中的位置
  7. 在onTouch事件中实现自由移动的功能,同时在移动的时候添加边界检测checkBorderWhenTranslate。
  8. 双击放大缩小:使用GestureDetector中的子类事件监听SimpleOnGestureListener手势实现双击的效果,使用AutoScaleRunnable进行缓慢的缩放。
    9.通过使用 getParent().requestDisallowInterceptTouchEvent(true);//通知父布局事件不被拦截,由当前控件进行事件处理。

    源码下载

0 0