View 事件分发流程

134 阅读10分钟

github.com/ccnio/Wareh…

分发流程概要

dispatchTouchEvent()、onTouchEvent()、onInterceptTouchEvent() 这3个方法的默认实现情况下:调用下层的方法 & 逐层返回,事件传递情况:(呈U型)

   1. 从上往下调用dispatchTouchEvent():Activity A ->> ViewGroup B ->> View C
2. 从下往上调用onTouchEvent(): View C ->> ViewGroup B ->> Activity A

 view 没有 onInterceptTouchEvent 方法, ViewGroup 才有。

// ViewGroup  
public boolean dispatchTouchEvent(MotionEvent ev) {  
    boolean result = false;             // 默认状态为没有消费过  
  
    if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View  
        result = child.dispatchTouchEvent(ev); // 结果false的话父view还会再处理  
    }  
  
    if (!result) { // 如果事件没有被消费,询问自身onTouchEvent  
        result = onTouchEvent(ev);  
    }  
    return result;  
}  
  
// View  
public boolean dispatchTouchEvent(MotionEvent event) {    
    if ((mViewFlags & ENABLED_MASK) == ENABLED &&   
          mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {    
        return true;    
    }   
  
    return onTouchEvent(event);    
}  

分发流程分析

主要包括:Activity事件分发机制、ViewGroup事件分发机制、View事件分发机制

Activity的事件分发

核心方法:dispatchTouchEvent() 、onTouchEvent()
入口 ViewRootImpl->WindowInputDispatcherReceiver::onInputEvent->DecorView::dispatchTouchEvent

@Override  
public boolean dispatchTouchEvent(MotionEvent ev) {  
    final Window.Callback cb = mWindow.getCallback();  
    // 交给应用Activity的dispatchTouchEvent处理触控事件  
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0  
        ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);  
}  

Window.Callback指向当前Activity

public boolean dispatchTouchEvent(MotionEvent ev) {  
    ...  
    // 先交给窗口PhoneWindow->DecorView->ViewGroup 处理  
    if (getWindow().superDispatchTouchEvent(ev)) {  
        return true;  
    }  
    // PhoneWindow没有消费事件则继续交给Activity的onTouchEvent继续处理  
    return onTouchEvent(ev);  
}  
  

Activity中会先将事件交给PhoneWindow处理,实际上就是交给DecorView中处理。

ViewGroup的事件分发

核心方法:dispatchTouchEvent()、onTouchEvent() 、onInterceptTouchEvent()。

onInterceptTouchEvent

onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用。

ViewGroup 拦截且消费事件
即 onInterceptTouchEvent、onTouchEvent()返回true时,那么后续其他事件(Move、Up)将直接传递给此View的onTouchEvent(),不再经过 onInterceptTouchEvent,此时 TouchTarget 也会为空。

拦截DOWN的后续事件
若 ViewGroup 拦截了一个半路的事件(如MOVE),该事件将会被系统变成一个CANCEL事件 & 传递给之前处理该事件的子View:

  • 该事件不会再传递给ViewGroup 的onTouchEvent()
  • 只有再到来的事件才会传递到ViewGroup的onTouchEvent()
  • 触发子view的cancel时会将执行TouchTarget的清空

所以后续事件将直接传递给ViewGroup B 的onTouchEvent()处理,而不会再传递给ViewGroup B 的onInterceptTouchEvent(),因该方法一旦返回一次true,就再也不会被调用了。

Down 事件的影响

先说一些细节,再看源码:

  • down 事件时会清除 TouchTarget,重置一些事件状态。
  • down 事件影响 move、up事件的处理:
    • down事件如果父控件没有拦截。 就会去找能处理该事件的子view,找到的话就将该子view赋值给mFirstTouchTarget。后续的 move、up事件 由于mFirstTouchTarget不为空,还会继续先经过父view的拦截判断才决定是否交给子 view(参见下面源码),如果原来子 View 处理了 down 事件,后面的 move/up 又被父 view 拦截了,那将会触发该子 View 的 Cancel 事件。
    • down事件如果被父控件消费了(消费)。mFirstTouchTarget也没有被赋值的机会,故后续的move和up事件均不会走拦截。
    • 由上述两条可见 onTouchEvent 的在 未设置 disallowIntercept 的执行时机:1. down 事件;2. 有子 View 处理事件。

TouchTarget 记录事件消费的子 View

TouchTarget的作用场景在事件派发流程中,用于记录派发目标,即消费了事件的子view。在ViewGroup中有一个成员变量mFirstTouchTarget,作为TouchTarget链表的头节点,使用链表是因为存在多点触摸情况下,需要将事件拆分后派发给不同的child。

  
private static final class TouchTarget {  
    // target 相关的 view  
    public View child;  
    // 当前 target 所绑定的 pointerId,利用2进制整数来判断  
    public int pointerIdBits;  
    // 使用链表维护多个 target  
    public TouchTarget next;  
    //...  
}  
  

若当次ev是ACTION_DOWN,则对当前ViewGroup来说,是一次全新的事件序列开始,那么需要保证清空旧的TouchTarget链表,以保证接下来mFirstTouchTarget可以正确保存派发目标。

在ViewGroup.dispatchTouchEvent()遇到非拦截事件,且事件类型为ACTION_DOWN或ACTION_POINTER_DOWN,则会触发一个遍历子控件以查找"触摸目标"的流程。TouchTarget是对消耗事件的View以链表方式保存,且记录各个View对应的触控点列表,以实现后续的事件派分处理。
同时可以推理出:

1. 非多点触控:mFirstTouchTarget链表退化成单个TouchTarget对象。
2. 多点触控,目标相同:同样为单个TouchTarget对象,只是pointerIdBits保存了多个pointerId信息。
3. 多点触控,目标不同:mFirstTouchTarget成为链表。

image.png 假设childA、childB都能响应事件:

  • 当触摸点1落于childA时,产生事件ACTION_DOWN,ViewGroup会为childA生成一个TouchTarget,后续滑动事件将派发给它。
  • 当触摸点2落于childA时,产生ACTION_POINTER_DOWN事件,此时可以复用TouchTarget,并给它添加触摸点2的ID。
  • 当触摸点3落于childB时,产生ACTION_POINTER_DOWN事件,ViewGroup会再生成一个TouchTarget,此时ViewGroup中有两个TouchTarget,后续产生滑动事件,将根据触摸点信息对事件进行拆分,之后再将拆分事件派发给对应的child。

ViewGroup 事件处理流程

接下来看 ViewGroup 的事件分发大概流程:

//源码分析:ViewGroup.dispatchTouchEvent()  
public boolean dispatchTouchEvent(MotionEvent ev) {   
      ...   
  
    // Down 事件重置状态:FristTouchTarget 清除  
    if (actionMasked == MotionEvent.ACTION_DOWN) {  
        // 清空 FristTouchTarget, mFristTouchTarget 置为空。  
        cancelAndClearTouchTargets(ev);  
        resetTouchState();  
    }  
  
//判定当前事件是否需要拦截:down 或 其它事件且mFristTouchTarget不为空(说明已经找到了该事件的目标消费者)  
    final boolean intercepted;  
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {  
        //可由View调用requestDisallowInterceptTouchEvent设置标记  
        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 {  
       //没有触摸targets 且 非ACTION_DOWN,所以需要 自己处理  
        intercepted = true;  
    }     
      
  if (!intercepted) {  
    // 逆序遍历子view,即先查询上面的,并调用 child.dispatchTouchEvent  
    for (int i = count - 1; i >= 0; i--) {    
        final View child = children[i];    
        if (frame.contains(scrolledXInt, scrolledYInt)) {    
            // 事件传递到了子View  
            if (child.dispatchTouchEvent(ev))  {   
                // 子View把 Down 事件消费掉了,并赋值 mFirstTouchTarget  
                mFirstTouchTarget = child;   
                return true;   
             }    
          }    
      }    
   }    
  ...  
  
    boolean handled;  
    if (mFirstTouchTarget == null) {  
        // ViewGroup自己处理事件  
        handled = super.dispatchTouchEvent(ev);  
    } else {  
        TouchTarget target = mFirstTouchTarget;//遍历链表,使用链表是因为多指触摸  
        while (target != null) { //target view处理事件  
            final TouchTarget next = target.next;  
            final boolean cancelChild = resetCancelNextUpFlag(target.child)  
                || intercepted;   
            // 派发给子 View cancel 事件  
            if (dispatchTransformedTouchEvent(ev, cancelChild,  
                    target.child, target.pointerIdBits)) {  
                handled = true;  
            }  
            ...  
            target = next;  
        }  
    }      
  
  return handled;  
}  
  
public boolean onInterceptTouchEvent(MotionEvent ev) {    
    return false;// 默认不拦截  

View的事件分发

核心方法:dispatchTouchEvent()、onTouchEvent()。

onTouch和onTouchEvent

  • onTouch方法:onTouch方法是View的 OnTouchListener接口中定义的方法。当一个View绑定了OnTouchLister后,当有touch事件触发时,就会调用onTouch方法。
  • onTouchEvent方法:onTouchEvent方法是override 的Activity、View的方法。当屏幕有touch事件时,此方法就会别调用。

当给一个view添加了onTouch事件的监听后,事件首先是onTouch方法执行,如果onTouch返回值为true,表示这个touch事件被onTouch方法处理完毕,不会把touch事件再传递给Activity/View,也就是说onTouchEvent方法不会被调用,如果onTouch的返回值是false,表示这个touch事件没有被view完全处理,onTouch返回以后,touch事件被传递给Activity/View,onTouchEvent方法被调用。

onTouch()、onTouchEvent()、onClick()是依序执行的。若执行到View的onTouchEvent,默认情况下:可点击,就一定返回true;不可点击就一定返回false。

public boolean dispatchTouchEvent(MotionEvent event) {   
    //如果view不可用 或者mOnTouchListener不为空且ontouch返回了真  
    // 那就调用不到 onTouchEvent 方法  
    if ( (mViewFlags & ENABLED_MASK) == ENABLED &&   
          mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {    
        return true;    
    }   
  
    return onTouchEvent(event);    
}  
  
public boolean onTouchEvent(MotionEvent event) {    
  ...  
  
  // 若该控件可点击,则进入switch判断中  
  if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {    
    // 根据当前事件类型进行判断处理  
    switch (event.getAction()) {   
        case MotionEvent.ACTION_UP:  
                performClick(); // up 处理 click 事件  
                break;    
        case MotionEvent.ACTION_DOWN:  
            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());    
            break;    
        case MotionEvent.ACTION_CANCEL:  
            refreshDrawableState();    
            removeTapCallback();    
            break;  
        case MotionEvent.ACTION_MOVE:  
            final int x = (int) event.getX();    
            final int y = (int) event.getY();    
  
            int slop = mTouchSlop;    
            if ((x < 0 - slop) || (x >= getWidth() + slop) ||    
                    (y < 0 - slop) || (y >= getHeight() + slop)) {    
                removeTapCallback();    
                if ((mPrivateFlags & PRESSED) != 0) {    
                    removeLongPressCallback();    
                    mPrivateFlags &= ~PRESSED;    
                    refreshDrawableState();    
                }    
            }    
            break;    
    }    
  
    // 若该控件可点击,就一定返回true  
    return true;    
  }    
  // 默认情况下若该控件不可点击,就一定返回false  
  return false;    
}  

ACTION_CANCEL

如果某一个子 View 已处理了 Down 事件,那么随之而来的 Move、Up事件也会交给它处理。但是交给它处理之前,父 View还是可以拦截事件的,如果拦截了事件,那么子 View 就会收到一个 Cancel 事件,并且不会收到后续的 Move、Up 事件。
解释参见上面 ViewGroup 的事件分发。

常见场景就是ListView中Item内部有一个Button,我们让ACTION_DOWN落在这个Button上,然后上下滑动,此时MOVE事件就会被ListView拦截,那么Button就会收到ACTION_CANCEL事件了。

多指触控

MotionEvent
Android中,事件信息通过MotionEvent来展现,可以用event.getAction()或者getActionMasked()来获取具体事件(两者区别接下来会说明),常用的事件有如下
MotionEvent.ACTION_DOWN:第一个触点被按下时触发
MotionEvent.ACTION_MOVE:有触点移动时触发,所有触点的移动都是这个事件
MotionEvent.ACTION_UP:最后一个触点放开时触发
MotionEvent.ACTION_CANCEL:见上段
MotionEvent.ACTION_POINTER_DOWN:多个触点时,按下非第一个点时触发
MotionEvent.ACTION_POINTER_UP:多个触点时,松开非最后一个点时触发

@Override  
public boolean onTouchEvent(MotionEvent event) {  
    int action = event.getAction();   
    int actionMasked = action & MotionEvent.ACTION_MASK;//得到事件类型 或者通过 event.getActionMasked()  
      
    switch (actionMasked) {  
        case MotionEvent.ACTION_DOWN:  
            Log.e("lijf", "onTouchEvent: ACTION_DOWN");  
            break;  
        case MotionEvent.ACTION_MOVE:  
            Log.e("lijf", "onTouchEvent: ACTION_MOVE");  
            break;  
        case MotionEvent.ACTION_POINTER_DOWN:  
            Log.e("lijf", "onTouchEvent: ACTION_POINTER_DOWN");  
            break;  
        case MotionEvent.ACTION_POINTER_UP:  
            Log.e("lijf", "onTouchEvent: ACTION_POINTER_UP");  
            break;  
    }  
    return true;  
}  

getActionMasked()、getAction()
Android用一个32位的整数值来表示一个TouchEvent事件,低8位表示Touch事件的具体动作,例如按下、抬起、移动等动作。高8位表示Touch事件中多点触控的索引值。

  • getAction:表示触摸动作的原始32位信息,包括Touch事件的具体动作和触控点信息。

  • getActionMasked:表示触摸的动作,按下、抬起、移动等信息。getActionMasked = getAction & MotionEvent.ACTION_MASK(0xff),所以getActionMasked方法只保留了低8位的信息,也就是说只保留了触摸的动作信息。

  • getActionMasked()、getAction():只有一个触点时,两者返回结果是一致的;多个触点时,返回值就会有差别了。

  • getActionIndex:表示多点触控中触控点的信息。
    getAction()中包含有:值的前8位与pointerIndex有关,后8位代表事件类型。pointerIndex稍后会介绍到。
    如果要从 getAction() 中获取事件类型,那就需要进行一些运算处理了,MotionEvent也提供了掩码分别获取用来获取pointerIndex、事件类型,ACTION的掩码:

public static final int ACTION_MASK                = 0xff; //用于获取低8位  
public static final int ACTION_POINTER_INDEX_MASK  = 0xff00; //用于获取高8位,再右移8位即pointerIndex  

当有多个手指在屏幕上操作时,如何区分是哪根手指的操作哪,又该如何获取这根手指的手势信息哪?

pointerId、pointerIndex
MotionEvent对象会存储当前所有触点(pointer)的信息,即使多个触点只有一个在活动。

  • pointerId,pointer从down到up之间一直是不变的,注意当此pointer失效后,它的pointerId会被新来的pointer使用。
  • pointerIndex,其值范围[0,getPointerCount()),但不像pointerId,pointerIndex对于同一个触点是会变化的。比如有两个pointer,它们的index分别为0、1,当index为0的pointer失效后,剩余pointer的index就会变成0。

两者可以互相转换:

getPointerId(int pointerIndex)
findPointerIndex(int pointerId)

MotionEvent类中的很多方法需要一个pointerIndex值作为参数。比如getX(pointerIndex)、getY(pointerIndex),返回的就是pointerIndex所代表的触摸点相关事件坐标值。pointer的id在整个事件流中是不会发生变化的,但是pointerIndex会发生变化。所以,要记录一个触点的事件流时,就需要保存其id,然后使用findPointerIndex(int)来获得其index值,然后再获得其他信息。

getActionIndex()获取该事件是哪个指针(手指)产生的。
getPointerCount()获取在屏幕上手指的个数。
getPointerId(int pointerIndex)获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
findPointerIndex(int pointerId)通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。
getX(int pointerIndex)获取某一个指针(手指)的X坐标
getY(int pointerIndex)获取某一个指针(手指)的Y坐标