在上一篇Android:手把手教你打造可缩放移动的ImageView最后提出了一个注意点:当自定义的MatrixImageView如ViewPager、ListView等带有滑动效果的ViewGroup中时,ImageView自定义的拖动事件会和ViewGroup的滑动事件冲突,并且指出了冲突原因是由于ViewGroup拦截了Move事件的缘故。如果对于Touch事件的分发机制不甚了解的话,可以参考下这篇Android:30分钟弄明白Touch事件分发机制

这篇文章将会在MatrixImageView的基础上,以ViewPager作为测试容器做进一步优化。

实现功能

  1. 当进行缩放操作时,手势不会同时触发ViewPager的滑动切换Item事件。
  2. 当进行拖动操作时,除非图片已经到达左右边界,否则不触发ViewPager的滑动切换Item事件。
  3. 当进行拖动操作时,若图片左边缘到达左边界,则可以向左滑动触发ViewPager切换至前一个Item;当图片右边缘到达右边界,则可以向右滑动触发ViewPager的切换至后一个Item。
  4. 每个down-up(cancel)事件周期内只执行一种类型的事务操作(缩放、拖动或者ViewPager切换Item),防止多重事务互相干扰。
  5. 将事务处理封装到MatrixImageView类内,提供状态接口给ViewPager使用,方便适配多种ViewGroup。

实现原理

当ViewPager内嵌MatrixImageView时,由于MatrixImgaeView在Down事件中返回了true,表明ViewPager将捕获本次完整的Touch事件(Move-Ponit_Down-UP等等),其中最重要的一个事件便是Move事件,因为ViewPager自身需要捕获Move事件在onTouch中进行切换Item操作,MatrixImageView的捕获意味着它将无法响应。不过,ViewPager本身控制着Touch事件的下发操作,每个Touch事件的下发都遵从从上至下层层递归,在MatrixImageView真正获得Move事件前,Move事件必须经过ViewPager的onInterceptTouchEvent和dispatchTouchEvent事件,前者执行拦截操作后者执行下发操作。ViewPager便是在onInterceptTouchEvent中对Move事件进行了过滤,当移动距离超过一定值时,它会拦截掉Move事件,阻止MatrixImageView继续处理Touch事件的权利,转而让自身的onTouch事件处理。于是,我们要做的便是重写onInterceptTouchEvent事件,通过判断MatrixImageView的状态决定是否拦截。

具体实现

由于容器ViewPager在满足条件的时候会拦截掉子View的touch事件,因此需要自定义个ViewPager修改拦截逻辑。当MatriImageView进行缩放和拖动时,我们不希望ViewPager拦截。具体代码如下:

public class AlbumViewPager extends ViewPager implements OnChildMovingListener {
/** 当前子控件是否处理拖动状态 */
private boolean mChildIsBeingDragged=false; @Override
public boolean onInterceptTouchEvent(MotionEvent arg0) {
if(mChildIsBeingDragged)
return false;
return super.onInterceptTouchEvent(arg0);
}
@Override
public void startDrag() {
// TODO Auto-generated method stub
mChildIsBeingDragged=true;
} @Override
public void stopDrag() {
// TODO Auto-generated method stub
mChildIsBeingDragged=false;
}
}
public interface OnChildMovingListener{
public void startDrag();
public void stopDrag();
}

通过判断变量mChildIsBeingDragged的值决定是否拦截,而mChildIsBeingDragged的值通过OnChildMovingListener接口由MatriImageView进行设置。别忘了在PagerAdapter的instantiateItem中给MatriImageView设置监听接口

    MatrixImageView imageView = (MatrixImageView) imageLayout.findViewById(R.id.image);
imageView.setOnMovingListener(AlbumViewPager.this);

ViewPager的改造便完成了,只需要新增一个变量和实现一个接口,之后对于事件的拦截操作都转到了MatrixImageView中。

接下去看下改造后的MatrixImageView的onTouch方法。

    /** 和ViewPager交互相关,判断当前是否可以左移、右移  */
boolean mLeftDragable;
boolean mRightDragable;
/** 是否第一次移动 */
boolean mFirstMove=false;
private PointF mStartPoint = new PointF();
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
//设置拖动模式
mMode=MODE_DRAG;
mStartPoint.set(event.getX(), event.getY());
isMatrixEnable();
startMove();
checkDragable();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
reSetMatrix();
stopMove();
break;
case MotionEvent.ACTION_MOVE:
if (mMode == MODE_ZOOM) {
setZoomMatrix(event);
}else if (mMode==MODE_DRAG) {
setDragMatrix(event);
}else {
stopMove();
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
if(mMode==MODE_UNABLE) return true;
mMode=MODE_ZOOM;
mStartDis = distance(event);
break;
case MotionEvent.ACTION_POINTER_UP: break;
default:
break;
}
return mGestureDetector.onTouchEvent(event);
}

其中加红部分的代码都是修改后的代码,逐一分析。

/**
* 子控件开始进入移动状态,令ViewPager无法拦截对子控件的Touch事件
*/
private void startDrag(){
if(moveListener!=null) moveListener.startDrag(); }
/**
* 子控件开始停止移动状态,ViewPager将拦截对子控件的Touch事件
*/
private void stopDrag(){
if(moveListener!=null) moveListener.stopDrag();
}

startDrag和stopDrag方法很简单,就是调用ViewPager传递进来的OnChildMovingListener接口进行mChildIsBeingDragged的设置。当监听到down事件时,表示开始拖动,当接收到up和cancel事件时,表示结束拖动,以这个逻辑来说,ViewGroup将永远无法拦截touch事件,所以我们还需要在其他地方设置stopDrag事件,后面说明。

接下去是在down事件中执行checkDragable方法:

        /**
* 根据当前图片左右边缘设置可拖拽状态
*/
private void checkDragable() {
mLeftDragable=true;
mRightDragable=true;
mFirstMove=true;
float[] values=new float[9];
getImageMatrix().getValues(values);
//图片左边缘离开左边界,表示不可右移
if(values[Matrix.MTRANS_X]>=0)
mRightDragable=false;
//图片右边缘离开右边界,表示不可左移
if((mImageWidth)*values[Matrix.MSCALE_X]+values[Matrix.MTRANS_X]<=getWidth()){
mLeftDragable=false;
}
}

该方法将会重置mLeftDragable、mRightDragable、mFirstMove三个参数的状态。mLeftDragable表示当前状态下的Matrix可以向左拖动,mRightDragable表示当前状态下的Matrix可以向右拖动,mFirstMove为每次完整touch事件(从down到up或cancel)中的第一次拖动操作标志。其中mLeftDragable和mRightDragable都是根据Matrix矩阵的数值计算出来的。

由于前面功能需求的时候说过"每个down-up(cancel)事件周期内只执行一种类型的事务操作(缩放、拖动或者ViewPager切换Item)",因此当进行缩放操作时,就不会再执行切换Item操作了,可以等缩放结束后执行up操作时stopDrag。而Move操作重点就是要识别是切换item还是拖动图片了。查看修改后的setDragMatrix代码

/**
* 设置拖拽状态下的Matrix
* @param event
*/
public void setDragMatrix(MotionEvent event) {
if(isZoomChanged()){
float dx = event.getX() - mStartPoint.x; // 得到x轴的移动距离
float dy = event.getY() - mStartPoint.y; // 得到x轴的移动距离
//避免和双击冲突,大于10f才算是拖动
if(Math.sqrt(dx*dx+dy*dy)>10f){
mStartPoint.set(event.getX(), event.getY());
//在当前基础上移动
mCurrentMatrix.set(getImageMatrix());
float[] values=new float[9];
mCurrentMatrix.getValues(values);
dy=checkDyBound(values,dy);
dx=checkDxBound(values,dx,dy); mCurrentMatrix.postTranslate(dx, dy);
setImageMatrix(mCurrentMatrix);
}
}else {
stopDrag();
}
} /**
* 和当前矩阵对比,检验dx,使图像移动后不会超出ImageView边界
* @param values
* @param dx
* @return
*/
private float checkDxBound(float[] values,float dx,float dy) {
float width=getWidth();
if(!mLeftDragable&&dx<0){
//加入和y轴的对比,表示在监听到垂直方向的手势时不切换Item
if(Math.abs(dx)*0.4f>Math.abs(dy)&&mFirstMove){
stopDrag();
}
return 0;
}
if(!mRightDragable&&dx>0){
//加入和y轴的对比,表示在监听到垂直方向的手势时不切换Item
if(Math.abs(dx)*0.4f>Math.abs(dy)&&mFirstMove){
stopDrag();
}
return 0;
}
mLeftDragable=true;
mRightDragable=true;
if(mFirstMove) mFirstMove=false;
if(mImageWidth*values[Matrix.MSCALE_X]<width){
return 0; }
if(values[Matrix.MTRANS_X]+dx>0){
dx=-values[Matrix.MTRANS_X];
}
else if(values[Matrix.MTRANS_X]+dx<-(mImageWidth*values[Matrix.MSCALE_X]-width)){
dx=-(mImageWidth*values[Matrix.MSCALE_X]-width)-values[Matrix.MTRANS_X];
}
return dx;
}

处理逻辑是这样的:

1.判断当前缩放级别是否是原始缩放级别(isZoomChanged()),如果未缩放过那将可以直接切换Item,在这直接stopDrag。

2.若进行了缩放,那判断是否累移动了10f,当移动了10f之后计算出x轴和y轴的移动量,并且通过checkDyBound方法计算出y轴的真实移动量

3.进入checkDxBound方法,首先判断当前是否能够左移,如果不能左移而实际的x轴偏移量是向左的,那就返回x的偏移量为0,防止左移。同时,如果当前是第一次移动,那就表示本次不是左移操作,而是向前切换item,于是执行stopDrag方法,令ViewPager拦截掉对MatrixImageView的事件分发。另外在这里加入和Y轴偏移量的对比,是为了防止执行的是垂直方向的滑动而导致stopDrag,ViewPager自身对于X轴偏移量/2小于Y轴偏移量的情况是不当成切换Item意图的,这里设置为*0.4可以保证不冲突。

4.右移同理。

5.当第一次左移和右移判断结果都不是切换Item后,将mLeftDragable和mRightDragable都设置为true,表示可以正常移动了。之后就和单个MatrixImageView的拖动处理一样了。

到此便完成了内嵌到ViewGroup内的MatriImageView的改造。下面还有两点显示优化。

首先在reSetMatrix中加入了新的功能:当缩放后的图片高度未达到ImageView高度时,在up和cancel之后会将其Y轴居中,防止“放大图片-Y轴移动图片-缩小图片”导致图片位置不对称。异常图效果如下:

/**
* 重置Matrix
*/
private void reSetMatrix() {
if(checkRest()){
mCurrentMatrix.set(mMatrix);
setImageMatrix(mCurrentMatrix);
}else {
//判断Y轴是否需要更正
float[] values=new float[9];
getImageMatrix().getValues(values);
float height=mImageHeight*values[Matrix.MSCALE_Y];
if(height<getHeight()){
//在图片真实高度小于容器高度时,Y轴居中,Y轴理想偏移量为两者高度差/2,
float topMargin=(getHeight()-height)/2;
if(topMargin!=values[Matrix.MTRANS_Y]){
mCurrentMatrix.set(getImageMatrix());
mCurrentMatrix.postTranslate(0, topMargin-values[Matrix.MTRANS_Y]);
setImageMatrix(mCurrentMatrix);
}
}
}
}

优化了缩放操作的缩放x轴对称轴选择问题。在"图片放大-移动X轴-缩小图片"时,若直接以ImageView中心点为缩放原点,可能会导致缩放后的图片边缘离开ImageView边界。

出错图效果如下:

/**
* 设置缩放Matrix
* @param event
*/
private void setZoomMatrix(MotionEvent event) {
//只有同时触屏两个点的时候才执行
if(event.getPointerCount()<2) return;
float endDis = distance(event);// 结束距离
if (endDis > 10f) { // 两个手指并拢在一起的时候像素大于10
float scale = endDis / mStartDis;// 得到缩放倍数
mStartDis=endDis;//重置距离
mCurrentMatrix.set(getImageMatrix());//初始化Matrix
float[] values=new float[9];
mCurrentMatrix.getValues(values);
scale = checkMaxScale(scale, values);
PointF centerF=getCenter(scale,values);
mCurrentMatrix.postScale(scale, scale,centerF.x,centerF.y);
setImageMatrix(mCurrentMatrix);
}
} /**
* 获取缩放的中心点。
* @param scale
* @param values
* @return
*/
private PointF getCenter(float scale,float[] values) {
//缩放级别小于原始缩放级别时或者为放大状态时,返回ImageView中心点作为缩放中心点
if(scale*values[Matrix.MSCALE_X]<mScale||scale>=1){
return new PointF(getWidth()/2,getHeight()/2);
}
float cx=getWidth()/2;
float cy=getHeight()/2;
//以ImageView中心点为缩放中心,判断缩放后的图片左边缘是否会离开ImageView左边缘,是的话以左边缘为X轴中心
if((getWidth()/2-values[Matrix.MTRANS_X])*scale<getWidth()/2)
cx=0;
//判断缩放后的右边缘是否会离开ImageView右边缘,是的话以右边缘为X轴中心
if((mImageWidth*values[Matrix.MSCALE_X]+values[Matrix.MTRANS_X])*scale<getWidth())
cx=getWidth();
return new PointF(cx,cy);
}

通过判断图片宽度,决定是以ImageView中点为X轴缩放原点,还是以左右边缘为缩放原点。

目前为止MatrixImageView的功能基本完善了,具体代码还是放在我的Github上的照相机Demo。该View如果有问题的可以在这篇文章下留言或私信我。

Android:手把手教你打造可缩放移动的ImageView(下)的更多相关文章

  1. Android:手把手教你打造可缩放移动的ImageView(上)

    定义ImageView,实现功能如下: 1.初始化时图片垂直居中显示,拉伸图片宽度至ImageView宽度. 2.使用两根手指放大缩小图片,可设置最大放大倍数,当图片小于ImageView宽度时,在手 ...

  2. Android 手势检测实战 打造支持缩放平移的图片预览效果(下)

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/39480503,本文出自:[张鸿洋的博客] 上一篇已经带大家实现了自由的放大缩小图 ...

  3. Android 进阶 教你打造 Android 中的 IOC 框架 【ViewInject】 (下)

    上一篇博客我们已经带大家简单的吹了一下IoC,实现了Activity中View的布局以及控件的注入,如果你不了解,请参考:Android 进阶 教你打造 Android 中的 IOC 框架 [View ...

  4. 手把手教你打造一个心电图效果View Android自定义View

    大家好,看我像不像蘑菇-因为我在学校呆的发霉了. 思而不学则殆 丽丽说得对,我有奇怪的疑问,大都是思而不学造成的,在我书读不够的情况下想太多,大多等于白想,所以革命没成功,同志仍需努力. 好了废话不说 ...

  5. Android 进阶 教你打造 Android 中的 IOC 框架 【ViewInject】 (上)

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/39269193,本文出自:[张鸿洋的博客] 1.概述 首先我们来吹吹牛,什么叫Io ...

  6. 手把手教你打造ImageView支持手势放大缩小

    写在前面 最近有了新的任务,学习的时间比以前少了不少,Java回炉的文估计是得缓缓了,不过每周一篇尽量保质保量.最近感觉我文写的有点不好,因为我写东西除非必要,不然概念性的东西我基本上都是一笔带过…… ...

  7. 手把手教你打造一个纯CSS图标库

    来,干了这碗安利 写这篇文章的目的其实就是为了安利一下我的图标库:iconoo,所以,开门见山,star吧少年少妇们!(这样的我是不是应该要加个github互粉的团伙了?) 主题说完了,下面进入正题. ...

  8. android 手把手教您自定义ViewGroup(一)

    1.概述 在写代码之前,我必须得问几个问题: 1.ViewGroup的职责是啥? ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属 ...

  9. 手把手教你打造高效的 Kubernetes 命令行终端

    Kubernetes 作为云原生时代的操作系统,熟悉和使用它是每名用户的必备技能.本文将介绍一些提高操作 Kubernetes 效率的技巧以及如何打造一个高效的 Kubernetes 命令行终端的方法 ...

随机推荐

  1. HDU 1501 Zipper(DFS)

    Problem Description Given three strings, you are to determine whether the third string can be formed ...

  2. springboot整合jedisCluster

    maven依赖 springboot整合jedisCluster相当简单,maven依赖如下: <dependency> <groupId>org.springframewor ...

  3. C#写的window服务内存溢出

    浅谈c#垃圾回收机制(GC) 写了一个window服务,循环更新sqlite记录,内存一点点稳步增长.三天后,内存溢出.于是,我从自己的代码入手,查找到底哪儿占用内存释放不掉,最终明确是调用servi ...

  4. Flask第31课——include标签

    我们在上一节代码基础上增加一些代码,样式: 文件名index.html,代码: {% from 'macros/forms.html' import input %} <!DOCTYPE htm ...

  5. 线程池、及使用场景、线程安全封装、ConcurrentHashMap应用场景

    https://blog.csdn.net/sinbadfreedom/article/details/80467253  :1.HashMap与ConcurrentHashMap的区别与应用场景 h ...

  6. python 编码类型

    碰到各种编码混用, 则需要搞清楚. http://blog.csdn.net/lxdcyh/article/details/4018054. https://www.jianshu.com/p/a5b ...

  7. BL老师的建议,数学不好的,大数据一票否决--后赋从java转大数据

    __________________________ 作者:我是蛋蛋链接:https://www.zhihu.com/question/59593387/answer/167235075来源:知乎著作 ...

  8. [C++] 一个能够定时自毁的类的实现

    试想一下, 有没有这种需求: 对于每一个新的对象, 我们希望它能够在一定时间后自动销毁, 前提是我们没有在这段时间内给它发出重置信号. 这种需求其实是有的, 比如在电影里, 主角知道了一个反派不希望被 ...

  9. python值传递和指针传递

    a= 1def change_integer(a):        a=a+1        return a print change_integer(a)print a b= [1, 2, 3] ...

  10. x86 openwrt虚拟路由代理上网

    一.代理服务器设置 1.下载代理软件CCProxy 6.8 Build 2.设置如下 二.x86 路由设置 1.在/etc目录下编辑profile http_proxy= https_proxy= f ...