浅谈android的事件分发机制

一、点击事件介绍

在Android 的点击事件中我们涉及到ViewGroup类的三个方法:

  @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(TAG, "dispatchTouchEvent: ");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG, "onTouchEvent: ");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i(TAG, "onInterceptTouchEvent: ");
        return super.onInterceptTouchEvent(ev);
    }
其中view与activity只有dispatchTouchEvent(),onTouchEvent()这两个方法,而onInterceptTouchEvent()则是viewGroup独有的方法,我们平常说的点击事件传递也主要复写这三个方法,先来了解一下这几个方法:

(1)dispatchTouchEvent

dispatchTouchEvent即点击事件的分发,这个有点类网络的路由器,是其他点击事件的主要拦路虎,可以说这个方法决定了事件能不能接着向下传递,一般我们不会复写这个方法,返回默认值即可,如果返回true或者false,会发现事件都不会往下传递,那么返回false与true又有什么区别呢?返回false表明事件到这里不会再接着往下传递,而是返回给父viewGroup的OnTouchEvent去处理该事件,而返回 true表明事件既不会往下传递,也不会返回给父VIewGroup进行处理,而是自己把事件给消化掉,什么也不做

(2)onInterceptTouchEvent

onInterceptTouchEvent即事件的拦截器,问题来了,为什么有了一个事件分发,怎么还整了一个事件的拦截?岂不是多余的吗?其实仔细想一下就会发现,他并不多余,在上一层的dispatchTouchEvent返回默认值,事件就会传递到这个方法,有点像局域网内的内部IP,起到导流的作用。一般在处理点击事件冲突的时候,主要复写这个方法来判断是否需要拦截事件,如果返回true说明事件被该ViewGroup拦截了,事件也就不会传递到下一级的ViewGroup或者是View,直接由该ViewGroup处理;如果返回false,表明该ViewGroup不对事件做拦截处理,直接让事件流向下一级

(3)onTouchEvent

onTouchEvent 即事件的逻辑处理,在onInterceptTouchEvent方法中,如果反回true,事件就会流到这个方法。在onTouchEvent方法中如果返回true,那么表示消费了该事件,返回false则表示不消费该事件并将事件往上传递,交给其父viewGroup处理

另外,如果我们的view实现了View.OnTouchListener接口,并重写了onTouch方法,那么这个方法会比onTouchEvent方法优先调用,并且如果onTouch方法返回true的话,onTouchEvent将不会被调用

二、案例讲解

其实上面的几个方法,大家也或多或少有点了解了,接下来我会根据实例来讲解Android的点击事件的具体分发流程
先创建两个个自定义的CustomRel和CustomRel2,复写以上三个方法:
public class CustomRel extends RelativeLayout{

    private static final String TAG = "CustomRel";

    public CustomRel(Context context) {
        super(context);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(TAG, "dispatchTouchEvent: ");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG, "onTouchEvent: ");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i(TAG, "onInterceptTouchEvent: ");
        return super.onInterceptTouchEvent(ev);
    }
}
其布局如下,CustomRel嵌套着CustomRel2

我们就一个一个的来验证,注意:以下均用控制变量法来验证,即只修改一处,其他均为默认值

(1)dispatchTouchEvent

先修改CustomRel2 的dispatchTouchEvent方法,返回默认值,打印结果如下
09-19 16:20:14.915 2255-2255/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 16:20:14.915 2255-2255/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 16:20:14.916 2255-2255/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
09-19 16:20:14.916 2255-2255/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent: 
09-19 16:20:14.916 2255-2255/com.ujs.tounchtest I/CustomRel2: onTouchEvent: 
09-19 16:20:14.917 2255-2255/com.ujs.tounchtest I/CustomRel: onTouchEvent: 
我们可以看到,事件传递到CustomRel2 的onTouchEvent方法,由于我们在这个方法里面什么也不做,事件又重新交给一层的ViewGroup的onTouchEvent方法处理,这就是传说中的U型图
那么返回false会如何呢?打印如下:
09-19 16:27:23.613 15488-15488/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 16:27:23.613 15488-15488/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 16:27:23.613 15488-15488/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
09-19 16:27:23.613 15488-15488/com.ujs.tounchtest I/CustomRel: onTouchEvent: 
可以看到,事件传递到CustomRel2 的dispatchTouchEvent方法时候,由于返回了false,CustomRel2 的其他两个方法均不会调用,而是直接将事件返回给上一层ViewGroup
onTouchEvent方法进行处理
返回true呢?
09-19 16:32:38.974 24932-24932/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 16:32:38.974 24932-24932/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 16:32:38.974 24932-24932/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
09-19 16:32:38.995 24932-24932/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 16:32:38.996 24932-24932/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 16:32:38.996 24932-24932/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
发现事件到了CustomRel2 的dispatchTouchEvent方法后就被消耗掉了,既不调用之后的两个方法也不会返回给上一层ViewGroup进行处理

(2)onInterceptTouchEvent

通过上面的讲解,我们知道onInterceptTouchEvent是用来对事件的拦截的,那么我们就来修改CustomRel的onInterceptTouchEvent方法,反回默认值与最刚开始的打印结果一致,直接修改返回false来看看:
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent: 
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel2: onTouchEvent: 
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel: onTouchEvent: 
结果与直接返回默认值一致,呈现U型传递
返回true试试看:
09-19 16:44:47.341 15717-15717/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 16:44:47.342 15717-15717/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 16:44:47.342 15717-15717/com.ujs.tounchtest I/CustomRel: onTouchEvent: 
我们发现,事件直接交给了CustomRel的onTouchEvent方法处理,没有向下传递,即事件被拦截掉了

(3)onTouchEvent

先重写两个类的onTouchEvent方法,获取点击事件的属性:
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "onTouchEvent: down ");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "onTouchEvent: move");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "onTouchEvent: up ");
                break;
        }
        return super.onTouchEvent(event);
    }
这里也有一个返回值,先返回默认的
跟预期一样,结果如下:
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent: 
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel2: onTouchEvent: down 
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel: onTouchEvent: down 
我们将CustomRel2 的onTouchEvent方法返回true看看
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent: 
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel2: onTouchEvent: down 
09-19 17:04:19.981 5766-5766/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 17:04:19.981 5766-5766/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 17:04:19.981 5766-5766/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
09-19 17:04:19.982 5766-5766/com.ujs.tounchtest I/CustomRel2: onTouchEvent: up 
我们可以看到CustomRel的onTouchEvent方法并没有获得事件,CustomRel2直接将事件消耗掉了
而返回false与返回默认值一样,均会将事件回传给上一层viewGroup的onTouchEvent方法
09-19 17:08:46.785 10070-10070/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 17:08:46.785 10070-10070/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 17:08:46.785 10070-10070/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
09-19 17:08:46.785 10070-10070/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent: 
09-19 17:08:46.786 10070-10070/com.ujs.tounchtest I/CustomRel2: onTouchEvent: down 
09-19 17:08:46.786 10070-10070/com.ujs.tounchtest I/CustomRel: onTouchEvent: down 
以上是针对子ViewGroup来说的,那如果父ViewGroup的onTouchEvent返回true呢?会把事件消耗掉吗?将CustomRel的onTouchEvent方法返回值修改为true,我们发现第一次事件传给子层后,CustomRel就直接把事件给消耗掉了,也就是说,当onTouchEvent反回true的时候,之后的事件默认就交给该ViewGroup的来处理,不再将事件向下传递
09-19 17:19:04.110 20055-20055/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 17:19:04.111 20055-20055/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent: 
09-19 17:19:04.111 20055-20055/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent: 
09-19 17:19:04.111 20055-20055/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent: 
09-19 17:19:04.111 20055-20055/com.ujs.tounchtest I/CustomRel2: onTouchEvent: down 
09-19 17:19:04.112 20055-20055/com.ujs.tounchtest I/CustomRel: onTouchEvent: down 
09-19 17:19:04.160 20055-20055/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent: 
09-19 17:19:04.160 20055-20055/com.ujs.tounchtest I/CustomRel: onTouchEvent: move

以上建议大家自行跑一遍就能体会了

三、源码分析

我们不仅要知其然,还要之气所以然,现在就参照源码,讲讲点击事件的分发为什么会是这样的。
首先我们要知道在Android的点击事件中主要分为两种:
1、按键的点击事件
2、屏幕的触摸事件

按键的事件主要由Wms来完成分发的 ,我们在实际中一般只考虑屏幕的触摸事件,在处理view的事件时有以下几点需要注意的:

1、屏幕触摸事件是直接分发给应用程序
2、子视图优先于父视图处理事件,即只有子视图消耗该事件了,父视图才有机会处理
3、在处理触摸事件时,需要根据触摸坐标来计算应该将事件分发给哪一个view/ViewGroup
我们先从DecorView入手,在该类的dispatchTouchEvent方法中有如下代码:
   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

这里先获取mWindow的callbak对象,如果没有就直接调用 基类的dispatchToucnEvent()方法
在ViewGroup中采用递归的方式分发事件:
1、触摸事件首先会把消息派发给view树中的最后一个子视图。如果该View没有消耗该事件,才会递归派发给父视图
在处理Down 消息的时候,其作用是判断点击事件是落在该ViewGroup的哪一个视图中


再来看看ViewGroup的dispatchTouchEvent方法
 (1)当MotionEvent的消息为Down的时候 ,先判断自身是否被禁止获取Touch消息,如果没有禁止,并且回调函数 onInterceptTouchEvent()中没有消耗该消息,就可以接着往下传递给子视图

     // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

然后开始根据坐标寻找子视图,主要作用是将布局坐标转化为视图坐标,获取到子view,在判断该子view 是否是ViewGroup,如果是就接着调用dispatchTouchEvent方法,否则递归结束

    final int childrenCount = mChildrenCount;
    if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    // Find a child that can receive the event.
    // Scan children from front to back.
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null
     && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
      childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
    preorderedList, children, childIndex);
     ...............
    }
   }

(2)当MotionEvent消息为UP或者cancel的时候,就清除 mGroupFlags 的FLAG_DISALLOW_INTERCEPT标志,即允许该ViewGroup截获消息。比如当用户释放手指,再次点击的时候,该ViewGroup可以再次截获消息,而当按下还没有释放期间,是不允许截获消息的

      // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
              }
   /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

2、在上面viewGroup 把消息传递到了view,先看view的dispatchTouchEvent这个方法,里面有下面几行代码

    public boolean dispatchTouchEvent(MotionEvent event) {
...............
      if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
.........................
}

可以看到,当onFilterTouchEventForSecurity方法返回true的时候才会调用OnTouch 跟onTouchAEvent 方法,onFilterTouchEventForSecurity代码如下

    /**
     * Filter the touch event to apply security policies.
     *
     * @param event The motion event to be filtered.
     * @return True if the event should be dispatched, false if the event should be dropped.
     *
     * @see #getFilterTouchesWhenObscured
     */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            return false;
        }
        return true;
    }

通过注释我们可以很清楚的看到,如果onFilterTouchEventForSecurity方法返回true,说明该消息应该分发到当前的view,否则不分发。我们注意到这一行注释

// Window is obscured, drop this touch.

如果当前Window 为模糊状态,那么他内部的所有view都应该为模糊效果,此时消息就不能分发到当前的view
回到dispatchTouchEvent方法中,判断当前mOnTouchListener是否为空,不为空就调用onTouch方法(注:OnTouch方法是View.OnTouchListener的抽象方法),如果onTouch返回true,那么就直接跳过onTouchEvent方法,否则调用onTouchEvent方法进行处理。这也就是为什么onTouch方法会优先于onTouchEvent方法的原因

三、总结 

至此我们就理顺了事件分发的三个方法的调用顺序以及返回值的作用,我们可以通过onInterceptTouchEvent的返回值来告诉系统我们是否需要拦截事件,通过onTouchEvent返回值来确定我们是否消耗点击事件,并对事件进行处理


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值