Android事件分发源码分析及事件冲突解决方法

621 阅读5分钟

Android开发中,事件冲突是老生常谈的问题,每次碰到事件冲突都觉得头大,一顿操作后,发现问题竟然解决了。通常事件冲突有两种解决方案

  • 外部拦截法:通过父容器去拦截事件,如果父容器需要处理事件就拦截,否则就不拦截,从而达到解决事件冲突问题
  • 内部拦截法:父容器不拦截任何事件,事件都传递给子元素去处理, 如果子元素需要此事件则直接处理掉,否则还给父容器去处理。这种处理方式需要配合requestDisallowInterceptTouchEvent才能正常工作,这种方案其实有个坑

事件分发流程

Activity dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
      ...
      if (getWindow().superDispatchTouchEvent(ev)) {
          return true;
      }
      return onTouchEvent(ev);
  }

Android 中我们知道window其实就是PhoneWindow, 所以其实就是调用PhoneWindow的superDispatchTouchEvent

PhoneWindow superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

mDecor其实就是我们非常熟悉的DecorView了

DecorView superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

所以到这里就清晰了,就是调用了ViewGroup的dispatchTouchEvent。ViewGroup 事件分发流程图网上一抓一大把,所以我们从源码角度去分析ViewGroup事件到底是如何分发的

ViewGroup dispatchTouchEvent

一个事件系列始于down事件,一系列move事件,终于up事件

  • down事件 从正常流程来分析 disallowIntercept在down事件值一定为false,为什么?往上面我们看到down事件里调用了 resetTouchState(),这个方法其实就是重置状态

    private void resetTouchState() {
        ...
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        ...
    }  
    

    这个方法里面重置了mGroupFlags的值,所以这也是为啥说disallowIntercept值为false原因 因此流程就来到了dispatchTouchEvent --> onInterceptTouchEvent
    假设ViewGroup 不去拦截事件,intercepted = false
    接下来我们看到这么一个方法块

     TouchTarget newTouchTarget = null;
     boolean alreadyDispatchedToNewTouchTarget = false;
     if (!canceled && !intercepted) {
     	...
     }
    

    正常情况下canceled=false,再看看这个方法块里面做了些什么事情?
    1、对ViewGroup 子View 根据Z轴做排序 buildTouchDispatchChildList()
    2、由上往下找到这个触摸事件可以给哪个子元素去处理

    ...
    if (!child.canReceivePointerEvents()
            || !isTransformedTouchPointInView(x, y, child, null)) {
          continue;
     }
    ...
    

    3、找到子元素之后

    dispatchTransformedTouchEvent() 里面有个关键性判断

    ...
     if (child == null) {
              handled = super.dispatchTouchEvent(transformedEvent);
      } else {
          final float offsetX = mScrollX - child.mLeft;
          final float offsetY = mScrollY - child.mTop;
          transformedEvent.offsetLocation(offsetX, offsetY);
          if (! child.hasIdentityMatrix()) {
              transformedEvent.transform(child.getInverseMatrix());
          }
    
          handled = child.dispatchTouchEvent(transformedEvent);
      }
    ...
    

    由上面分析得到,此时child就是ViewGroup上面能处理这个down 事件的子View, 所以child不为空 所以就调用了View 的dispatchTouchEvent, 这也就验证了我们开篇图的事件流程。
    4、假设我们的子View处理了事件 那么view dispatchTouchEvent返会了true, 上面图最后两行代码得出 mFirstTouchTarget 就是我们此次处理事件子View, 然后就退出循环, 这次事件结束 5、假设我们的子View不处理事件, 那么继续往上找父容器去处理, 这也是为何上面会对ViewGroup子View进行排序原因,

  • move事件
    1、由down事件分析得制,此时mFirstTouchTarget就是子View,重复down事件步骤,不同点在于不会再去寻找子View再去分发,事件由处理down 事件子View持有 2、由于mFirstTouchTarget 不为空, 参照down事件步骤3 所以这次move事件还是由子View去处理

  • up事件 类似move 事件流程

案例

使用外部拦截或内部拦截来分析事件流程

  • 外部拦截法代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            if(condication){
                return false;
            }
    }
    return true;
}

由上面事件分析流程我们可以看出,当父容器拦截事件返回true后,down事件中, !canceled && !intercepted 条件是不成立的,而此时mFirstTouchTarget = null, 因此直接就调用了dispatchTransformedTouchEvent(), 所以这时候事件直接由父容器处理。所以这个条件就决定了事件由父容器处理还是由子元素处理,这也是为何外部拦截法能处理事件冲突原因。

  • 内部拦截法代码
    父容器
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if(ev.getAction() == MotionEvent.ACTION_DOWN){
     	super.onInterceptTouchEvent(ev);
        return false;
    }
    return true;
}

子容器

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mAutoPlayAble) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_UP:
            	if(condication){
                	getParent().requestDisallowInterceptTouchEvent(false);
                }
            	break;
            default:
            	break;
            
        }
    }
    return super.dispatchTouchEvent(ev);
}

接着分析为何这种方式也可以直接处理事件冲突 1、down事件流程还是正常流程,此时事件由子View处理 2、move 事件中,如果符合子View move 事件, 那么disallowIntercept会被置为false 由于父容器onInterceptTouchEvent 返回true, 所以此时intercepted = true 3、 由于ihntercepted=true, 所以alreadyDispatchedToNewTouchTarget=false, 因此cancelChild = true, 再次回到dispatchTransformedTouchEvent 这代码块 event事件重置为ACTION_CALCEL, 由于child 还是那个子View, 所以这个事件还是给子View处理,只不过这时候事件类型是ACTION_CALCEL, 回到步骤3 由于cancelChild为true, 所以这时候mFirstTouchTarget = null 4、当第二个move事件进来后, 如何把事件从子View 还给父View? 接着分析。 5、由于第一个move事件把mFirstTouchTarget = null, 因此这时候intercepted = true, 直接回到这里 6、还是回到dispatchTransformedTouchEvent 这个方法,还是回到了最开始down事件一样流程 super.dispatchTouchEvent 所以事件又回到了父容器。
因此这种内部拦截法也能解决事件冲突, 唯一的坑就是down 事件中 父容器onInterceptTouchEvent 要返回false

Android 事件分发采用责任链模式,一层层往下传递,不处理就一层层往上传递。