通俗理解Android中View的事件分发机制及滑动冲突处理

说起Android滑动冲突,是个很常见的场景,比如SliddingMenu与ListView的嵌套,要解决滑动冲突,不得不提及到View的事件分发机制。

一、Touch事件传递规则分析
首先,我们要知道Touch事件是包装在MotionEvent对象中的,在手指与屏幕接触过程中产生一系列事件,典型的事件有以下三种:
ACTION_DOWN:手指刚接触屏幕的瞬间
ACTION_UP:手指刚离开屏幕的瞬间
ACTION_MOVE:手指在屏幕上滑动

那么,Android中Touch事件是一个怎样的传递过程呢?

   事件分发:public boolean dispatchTouchEvent(MotionEvent ev)

Touch事件发生时Activity的dispatchTouchEvent(MotionEvent ev)方法会将事件传递给最外层View的dispatchTouchEvent(MotionEvent ev)方法,该方法对事件进行分发。分发逻辑如下:
    如果return true,事件会由当前View的dispatchTouchEvent方法进行消费,同时事件会停止向下传递;
    如果return false,事件分发分为两种情况:
        如果当前 View 获取的事件直接来自 Activity,则会将事件返回给Activity的onTouchEvent进行消费;
        如果当前 View 获取的事件来自外层父控件,则会将事件返回给父View的onTouchEvent进行消费。

    如果return super.dispatchTouchEvent(ev),事件会自动的分发给当前View的onInterceptTouchEvent方法。

   事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)

上面已经提到,如果在dispatchTouchEvent返回super.dispatchTouchEvent(ev),那么事件将会传递到onInterceptTouchEvent方法,该方法对事件进行拦截。拦截逻辑如下:
    如果return true,则表示拦截该事件,并将事件传递给当前View的onTouchEvent方法;
    如果return false,则表示不拦截该事件,并将该事件交由子View的dispatchTouchEvent方法进行事件分发,重复上述过程;

    如果return super.onInterceptTouchEvent(ev),默认表示拦截该事件,并将事件传递给当前View的onTouchEvent方法,和return true一样。

   事件响应:public boolean onTouchEvent(MotionEvent ev)

上面已经提到,在dispatchTouchEvent(事件分发)返回super.dispatchTouchEvent(ev)并且onInterceptTouchEvent(事件拦截返回true或super.onInterceptTouchEvent(ev)的情况下,那么事件会传递到onTouchEvent方法,该方法对事件进行响应。响应逻辑如下:
    如果return true,则表示响应并消费该事件;
    如果return fasle,则表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true,如果到了最顶层View还是返回false,那么认为该事件不消耗,则在同一个事件系列中,当前View无法再次接收到事件,该事件会交由Activity的onTouchEvent进行处理;
    如果return super.dispatchTouchEvent(ev),则表示不响应事件,结果与return false一样。
这里也顺便说一下,如果一个View同时监听了onTouch事件和onClick事件,则在onTouchEvent里面应该返回false,否则点击事件就无法监听到。后面会提到这一点。

上述三个方法到底有什么区别与联系呢?我们通过一段伪代码来表示:

public boolean dispatchTouchEvent(MotionEvent ev){
	boolean consume = false;
	if(onInterceptTouchEvent(ev)){                  // 如果onInterceptTouchEvent返回true
		consume = onTouchEvent(ev);             // 则交由该View的onTouchEvent方法
	} else {
		consume = child. dispatchTouchEvent(ev); // 否则交由子View的dispatchTouchEvent事件进行分发
	}
	return consume; // 如果成功消费该事件,则返回true,然后停止传递,否则返回false
}
那么,接下来就总结一下事件的传递的规则。
    (1)当一个点击事件产生后,它的传递过程遵循的规则如下:Activity->Window->View。顶级View接收到事件之后,就会按相应规则去分发事件。如果一个View的onTouchEvent方法返回false,那么将会交给父容器的onTouchEvent方法进行处理,逐级往上,如果所有的View都不处理该事件,则交由Activity的onTouchEvent进行处理,就跟工作中遇到了难题,逐级找领导解决一个道理,领导解决不了,再找上一级领导。
    (2)正常情况下,一个事件序列只能被一个View拦截且消耗,某个View一旦进行事件拦截,那么这一个事件序列都只能交由他处理,并且onInterceptTouchEvent也不会被再次调用,因此,正常情况下一个事件是不能交给两个View来处理的,当然,特殊做法就是在View的onTouchEvent,处理完之后再返回false,强行交给其他View处理。
    (3)如果某一个View开始处理事件,如果他不消耗ACTION_DOWN事件(也就是onTouchEvent返回false),则同一事件序列比如接下来进行ACTION_MOVE,则不会再交给该View处理,就像工作中做一件事情,你要嘛做完,要嘛你就不要做这件事了。
    (4)在Android中,ViewGroup默认返回false,即不拦截任何事件。
    (5)诸如TextView、ImageView这些不作为容器的View,一旦接受到事件,就调用onTouchEvent方法,它们本身没有onInterceptTouchEvent方法。正常情况下,它们都会消耗事件(返回true),除非它们是不可点击的(clickable和longClickable都为false),那么就会交由父容器的onTouchEvent处理。
    (6)View的enable属性不影响onTouchEvent的默认返回值,只要它clickable或者longClickable为true,则onTouchEvent就会返回true。
    (7)点击事件分发过程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent-->OnClickListener的onClick方法。也就是说,我们平时调用的setOnClickListener,优先级是最低的,所以,onTouchEvent或OnTouchListener的onTouch方法如果返回true,则不响应onClick方法...


二、滑动冲突处理过程分析
滑动冲突的场景常见于滑动嵌套,就是一个页面中可能有两个或两个以上的View同时可以滑动,那么就可能会导致只有其中的一个View能滑动。一个最简单的屏幕触摸动作触发了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE—>...->ACTION_MOVE->ACTION_UP。
滑动冲突场景主要有三种:
(1)一个页面中同时存在左右滑动和上下滑动。
    让外部的View拦截滑动事件,判断滑动的特征,如果水平滑动距离>竖直滑动距离,则为水平滑动,反之为竖直滑动。假设内部View可以水平滑动,外部View可以竖直滑动,那么在外部View的onInterceptTouchEvent方法判断,如果触摸事件为水平滑动,则应该放行,也就是返回false,然后交给内部View来处理,那么内部子View就可以实现水平滑动。当然,还有一种方法就是外部View不拦截,交给内部View处理,如果内部View有需要就自己消耗掉,否则交给上一层,但是这样违反了事件分发机制,所以需配合requestDisallowInterceptTouchEvent(MotionEvent ev)进行处理,这里就不细说了,有兴趣的童鞋可以研究一下。
(2)同时存在两个竖直或水平滑动
    这个主要还得根据具体的需求分析。最简单的加入是两个ScrollView嵌套,一般可以判断ACTION_DOWN在那个View上,就执行那个View的滑动事件。
(3)就是(1)和(2)同时存在的情况
    实际上也得看具体业务需求找到突破点,但是处理方式本质上来说都是差不多的,就是要根据滑动策略,来干扰事件分发机制。

附上一段伪代码来理清一下思路:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) { // 外部View拦截事件
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
        }
        mLastXIntercept = x; // 分别记录上次滑动坐标
        mLastYIntercept = y;

        return intercepted; // 看是否需要传递给内部View处理
    }


参考:

Android开发艺术探索

你可能感兴趣的:(Android开发)