Skip to content

wangluAndroid/ScaleImageView

Repository files navigation

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


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


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

效果图.gif

一 . 具体分析

  • 想要进行图片的放大与缩小,起码要知道图片什么时间加载完毕吧,这里就要用到一个监听(OnGlobalLayoutListener:实现这个接口即可) 用来监听ImageView加载图片完毕 ; 注意:此监听有的小伙伴可能在Activity的onCreate方法中为了获得控件的宽高用过,对了,就是它,来监听ViewTree的变化,但是使用时需要在onAttachedToWindow中注册监听,在onDetachedFromWindow中移除监听,具体实现看下面代码;
  • 图片缩放要以手指触控的中心点进行缩放,并且缩小时需要处理边界问题,必须保证图片居中显示;这里就需要用到Matrix这个类和ScaleGestureDetector这个类; 下面先解释下Matrix这个类的使用方法:
    1. Matrix内部的值本质是个float类型的数组,为3*3的一维数组(float[9]),具体的含义为: 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(event)并返回true;**具体请看最下面附属的完整代码;
	@Override
	protected void onAttachedToWindow() {
		super.onAttachedToWindow();
		//注册onGlobalLayoutListener
		getViewTreeObserver().addOnGlobalLayoutListener(this);
	}

	@Override
	protected void onDetachedFromWindow() {
		super.onDetachedFromWindow();
		//移除onGlobalLayoutListener
		getViewTreeObserver().removeGlobalOnLayoutListener(this);
	}

	/**
	 * 捕获图片加载完成事件 onMeasure 和onDraw都不适合
	 */
	@Override
	public void onGlobalLayout() {
		//初始化的操作 一次就好  为了保证对缩放只进行一次
		if(!mOnce){
			
			//得到控件的宽和高--不一定是屏幕的宽和高 可能会有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(dx, dy);
			mMatrix.postScale(mInitScale, mInitScale, width/2, height/2);//以控件的中心进行缩放
			setImageMatrix(mMatrix);
			
			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(rect);
		}
		return rect ;
	}
  • 既然要缩放,那就要知道本次在上次的基础上缩放的比例,因此需要首先知道图片已经缩放的比例;得到图片的缩放值后,就需要在ScaleGestureDetector的内部接口OnScaleGestureListener的onScale方法中处理缩放逻辑,具体实现请看下面代码:
	/**
	 * 获取图片当前的缩放值
	 * @return
	 */
	public float getScale(){
		float[] values = new float[9];
		mMatrix.getValues(values);
		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()==null){
			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(mMatrix);
		}
		return true;
	}

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

  • 双击放大与缩小功能,需要重写GestureDetector类的两个参数的构造函数,第二个参数为OnGestureListener,具体实现类为SimpleOnGestureListener,只需要重写onDoubleTap()方法即可; 注意:需要在onTouch()方法最上面通过此代码mGestureDetector.onTouchEvent(event)传递给GestureDetector类进行双击控制,具体请看最先面附属的完整代码

二. 具体在xml中的实现如下:

    <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类型;

三. 由于此demo自定义的ImageView放在了ViewPager中,当图片放大了,左右滑动时会和ViewPager手势冲突,需要处理,本例子中的冲突在onTouch中做的处理,可以看下;其实处理这种冲突很简单,只需要分析出冲突在哪里,就在哪里进行处理即可;冲突一般有三种情况:

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

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

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

b.对于第二,三种情况

可以 根据业务写出处理规则, 比如当内部View滑动到顶部或者底部时响应外部View,我们就可以根据这个规则判断内部View有没有滑动到底, 如果有的话就不消费事件,没有的话就消费事件.具体怎么消费事件有两种方法. #####1.外部拦截法 所有的事件都要经由decorView分发,所以我们可以在decorView处做文章 如果父View需要事件,就拦截事件;否则就不拦截事件.具体实现在onInterceptTouchEvent()中处理.

public boolean onInterceptTouchEvent(MotionEvent event){
    boolean interceptd = false;
    //获取当前动作所在点
    int x = (int) event.getX();
    int y = (int) 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;
}

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

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

父元素要做出如下处理

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

默认拦截除了ACTION_DOWN以外的事件.这样子元素调用requestDisallowInterceptTouchEvent(false)父元素才能继续拦截所需事件(看情况处理);

###如有什么问题,敬请提出,十分感谢!希望越来越好,谢谢! ####如果喜欢,还请点击start,喜欢支持一下了,谢谢O(∩_∩)O~。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages