View事件体系简介

1.View的基础概念

  View是Android中控件的基类,是一种界面层控件的一种抽象,它代表的是一个控件。除了View,还有ViewGroup,从名字来看,它可以被翻译成控件组,言外之意就是ViewGroup中可以包含多个控件。而在Android设计中,ViewGroup也是继承了View,这意味着View本身就可以是单个控件也可以是多个控件组成的一组控件,这种关心与Web中DOM叔的概念类似。

2. View的位置参数

  View的位置主要由它的四个顶点来决定的,分别对应View的四个属性:left、top、right和right。但是这四种属性都是相对于parentView来定义的,也就是说它们是相对坐标。
  从Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY。其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父组件的偏移量。由此可见:x、y、translationX和translationY也是相对于父组件的相对坐标。它们满足的关系式为:

x = translationX + left

需要注意的是,View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变是x、y、translationX和translationY这四个参数。

3.一些基本概念

3.1 MotionEvent

在手指滑动屏幕的过程中,经过Android系统的封装,会产生一系列的事件,一般都描述为:

  1. ACTION_DOWN 手指接触到屏幕
  2. ACTION_MOVE 手指在屏幕上移动
  3. ACTION_UP 手指离开屏幕的一瞬间

正常的情况下,一次手指触摸屏幕的行为会触发一系列的点击事件,一般情况为:

  1. ACTION_DOWN —> ACTION_UP 手指点击屏幕后立即离开屏幕
  2. ACTION_DOWN —> ACTION_MOVE —> ACTION_MOVE —> … —> ACTION_UP 手指点击屏幕后滑动屏幕,最后离开屏幕。
    这种情况就是典型的时间序列,同时通过MotionEvent对象我们可以得到点击事件发生的坐标(x,y)。同时可以根据Android系统提供getX()/getRawX()、getY()/getRawY()进行相应的判断。
3.2 TouchSlop

TouchSlop是系统中所能识别的被认为是滑动的最小距离。该距离与设备相关,可以通过代码获取:

ViewConfiguration.get(getContext()).getScaledTouchSlop()
3.3 VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平速度和竖直速度。使用方式比较简单,首先在View的onTouchEvent方法中追踪当前单击事件的速度:

VelocityTraker velocityTracker = VelocityTraker.obtain();
velocityTrcaker.addMovement(event);

此时可以计算出当前的速度:

velocityTracker.computeCurrentVelocity(1000);
int velocityX = (int)velocityTracker.getXVelocity();
int velocityY = (int)velocityTracker.getYVelocity();

这一步需要注意两点:
1.获取速度之前必须先计算速度,获取velocityX或者velocityY之前需要调用computeCurrentVelocity方法;
2.这里的速度是指一段时间内手指所滑过的像素数目,比如将时间间隔设置为1000ms,在1s内手指在水平方向上从左向右滑过100像素,那么水平速度就为100.当然从右到左此时速度就为-100了。
最后当我们不需要velocityTracker时,需要调用clear方法来重置并回收内存:

velocityTracker.clear();
velocityTracker.recycle();

4. View事件机制

4.1 三个重要方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) 

用于事件分发。如果事件能传递到该View,那么此方法一定会调用。它表示是否消耗该事件(消耗返回true/不消耗返回false)

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) 

用于事件拦截,ViewGroup独有事件。如果该事件返回true,则标识本ViewGroup将会拦截本事件,并将调用ViewGroup的onTouchEvent。正常情况下,onInterceptTouchEvent在同一个事件序列中只会调用一次。

@Override
public boolean onTouchEvent(MotionEvent event) 

用来处理点击事件,返回结果标识是否消耗本事件,如果不消耗,那么在同一个事件序列中,当前View无法再次接受事件。

对于三个事件,一般可以描述为:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result ;
    if(onInterceptTouchEvent(event)){
      result = onTouchEvent(event);
    }else{
      result = child.dispatchTouchEvent(event);
    }
    return result; 
}

综合概述为:当事件被传递到ViewGroup时,ViewGroup的dispatchTouchEvent首先会被调用,如果本ViewGroup中onInterceptTouchEvent返回值为true,表明需要拦截本事件;那么将会调用ViewGroup的onTouchEvent。如果ViewGroup表明不拦截本次事件,将会将本次事件传递到子View中,进而继续调用dispatchTouchEvent,直到消耗事件被处理为止。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法被调用。此时事件如何处理还需要看onTouch的返回值,如果返回false,则当前的View的onTouchEvent方法将会被调用;如果返回true,那么onTouchEvent将不会被调用。

4.2 伪代码表示事件分发机制

在3.1中已经描述过,每次手指操作屏幕都会产生一系列的事件,那么我们需要对事件进行拆开分析:

  1. dispatchTouchEvent中如果我们的事件是ACTION_DOWN,那么此时的运行模式是:先询问onInterceptTouchEvent是否拦截该事件,然后找到将要处理该事件的viewTarget,伪代码为:
public boolean dispatchTouchEvent(MotionEvent ev) {
    View targetView = null ;
    
    if(onInterceptTouchEvent(ev)) {    //1.是否拦截
        return onTouchEvent(ev);
    }else {
       targetView = findMotionEventTargetView(ev);   //2.子view是否消耗
	   
	   if(null != targetView){
	   		return targetView.dispatchTouchEvent();
	   }else{
	   		return super.dispatchTouchEvent(ev);
     	}
    }
}

对于拦截的的条件分支,我们看到如下情况:
a. 目标targetView将保持为null
b. 将直接调用onTouchEvent方法

对于ViewGroup不拦截本事件,那么我们就需要寻找一个可以处理本事件的View,也就是targetView,具体的查看源码里面有,无非就是遍历所有的子View,然后查找合适的View,当然这个findMotionEventTargetView方法里面也是别有洞天的,大致伪代码为:

//ViewGroup#dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
       final boolean handled;

        //...代码省略

       handled = child.dispatchTouchEvent(transformedEvent);
      
       return handled;
}

如果这个方法dispatchTransformedTouchEvent返回false,那么我们的targetView将不会被找到,此时回到dispatchTouchEvent伪代码中,将会调用ViewGroup#super.dispatchTouchEvent(ev),而此时将会调用View.dispatchTouchEvent方法,dispatchTouchEvent中将会调用onTouchEvent将会被调用。

  1. 如果此时MotionEvent为非ACTION_DOWN,比如ACTION_MOVE、ACTION_UP、ACTION_CANCEL事件时,此时用伪代码表示为:
public boolean dispatchTouchEvent(MotionEvent ev) {
    if(null == targetView) {
        return onTouchEvent(ev);
    }else {
       return targetView.dispatchTouchEvent();
    }
}

此时需要用到ACTION_DOWN事件我们查找出来的targetView 了,如果targetView不存在,直接还是调用自身的onTouchEvent,而当我们找到了那个targetView时,事件处理将会出现在targetView.dispatchTouchEvent事件中。也许这就是这简单的事件分发机制了,下面来总结一下这个过程和其中发生的过程:
a. View事件分发时,首先会调用dispatchTouchEvent,在该方法中首先会询问自身的onInterceptTouchEvent是否会拦截此次事件;如果不拦截则遍历查找ViewGroup中所有子View,如果找到一个可以处理该事件的View,则会调用View的dispatchTouchEvent,进而会调用View的onTouchEvent,如果onTouchEvent返回值为false,表示将不会消耗该事件,那么说明此次事件不能被消费;如果返回为true,表明此次将会消费此次事件,并将此view标记为targetView
b. 如果找到targetView,接下来的ACTION_MOVEACTION_UPACTION_CANCEL等非ACTION_DOWN事件将会通过targetView.dispatchTouchEvent依次调用,并完成相应逻辑;如果没有找到targetView,那么该直接View将不再询问onIntercept方法,而是直接调用自身的onTouchEvent方法,完成相应的逻辑。

4.3 滑动冲突解决方式
4.3.1 外部拦截法

所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法主要是重写OnInterceptTouchEvent方法,在内部做相应的拦截处理即可,一般伪代码为:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercept = true;
                }

                break;

            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;

                if (父容器需要拦截当前事件) {
                    intercept = true;
                } else {
                    intercept = false;
                }

                break;

            default:
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }


        mLastX = x;
        mLastY = y;

        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercept;
    }

这个是典型的外部拦截逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件的条件即可,其他均不需要做修改并且也不能修改。在onInterceptTouchEvent方法中,首先是ACTION_DOWN事件,父容器必须返回false,即不拦截ACTION_DOWN事件。因为父容器一旦拦截了ACTION_DOWN,那么后续的ACTION_MOVEACTION_UP事件将会直接交给父容器处理,无法再传递给子元素;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则为false;最后是ACTION_UP事件,这里需要要返回false,因为ACTION_UP事件本身没有太多意义。

4.3.3 内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来比外部拦截法显得复杂。
伪代码如下,需要重写子元素dispatchTouchEvent方法:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                parentView.requestDisallowInterceptTouchEvent(true);
                break;

            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;

                Log.d(TAG,"deltaX :" + deltaX + ",deltaY:" + deltaY);

                //如果水平距离大于竖直距离,事件将会被父组件拦截
                //否则事件还是给与自己使用
                if(父类组件需要当前点击事件){
                    parentView.requestDisallowInterceptTouchEvent(false);
                }
                break;

            case MotionEvent.ACTION_UP:
                break;
        }

        mLastX = x ;
        mLastY = y ;

        return super.dispatchTouchEvent(ev);
    }

这就是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动且不能做改动。除了子元素需要做处理之外,父元素中也要默认拦截ACTION_DOWN以外的事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false),父元素才能继续拦截所需要的事件。

对于父容器不能拦截ACTION_DOWN事件?因为ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT标记位控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中,这样内部拦截就无法起作用了。父元素需要修改的伪代码为:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        int action = ev.getAction();

        if(action == MotionEvent.ACTION_DOWN){
            mLastX = x ;
            mLastY = y ;
            return false;
        }
        return true ;
    }

文中部分内容来自经典读物<<Android开发艺术探索>>,也算是自己对View事件的一个简单概括吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值