View事件分发

View中消息的传递

对于一颗View树,它的消息的传递是自上而下,即从根节点开始逐层往子类递归传递的。在消息传递的过程中,一旦有View处理了这个消息,那么传递即宣告终止。从这一点看View树的上层有消息的优先处理权。

一、View中TouchEvent的投递流程

事件的处理是通过多种形式的InputStage来分别处理,如NativePostImeInputStage、ViewPostImeInputStage、SyntheticInputStage、EarlyPostImeInputStage。这些都重载了onProcess()方法,以ViewPostImeInputStage为例:

 protected int onProcess(QueuedInputEvent q) {
     if (q.mEvent instanceof KeyEvent) {//按键事件
         return processKeyEvent(q);
     } else {
         final int source = q.mEvent.getSource();
         if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {//Pointer事件
            return processPointerEvent(q);
        } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {//Trackball事件
            return processTrackballEvent(q);
        } else {
            return processGenericMotionEvent(q);
        }
    }
}
sequenceDiagram
InputEventReceiver->>InputEventReceiver:dispatchInputEvent()
InputEventReceiver->>InputEventReceiver:onInputEvent()
InputEventReceiver->>ViewRootImpl: enqueueInputEvent()
ViewRootImpl->> ViewRootImpl:doProcessInputEvents()
ViewRootImpl->> ViewRootImpl:deliverInputEvent()
ViewRootImpl->> InputStage:deliver()
InputStage->>ViewPostImeInputStage:onProcess()
ViewPostImeInputStage->>ViewPostImeInputStage:processPointerEvent()
ViewPostImeInputStage->>ViewPostImeInputStage:processPointerEvent()
ViewPostImeInputStage->>View:dispatchPointerEvent()
View->>View:onTouch()
View->>View:onTouchEvent()

当系统判断当前是SOURCE_CLASS_POINTER类型的事件后,将会调用processPointerEvent()做进一步处理,这个函数会将这一处理权交给View Tree的根元素,即mView的dispatchPointerEvent接口,然后后者再细化判断。

View.java调用dispatchTouchEvent()负责事件分发:

public boolean dispatchTouchEvent(MotionEvent event) {
 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;
        }
}

View类事件分发主要考虑两个因素:

  • onTouch

    View通过setOnTouchListener来设置一个Event监听。这种方式先与onTouchEvent()

  • onTouchEvent

如果没有设置OnTouchListener或者mViewFlag != ENABLED 又或者onTouch返回false,那么系统会将Event传递给onTouchEvent。

上述两种情况再开发中都经常使用,onTouch更为简洁高效,onTouchEvent()更适用于View扩展类的情况(重载onTouchEvent)。

下面将分段阅读onTouchEvent

 public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
}

上面的情况即便在View disable的情况,也会消耗这个事件,只是不做任何回应而已。

如果View没有被disable,接下来程序将处理这一Touch事件,TouchEvent还可以细分为很多种类型,即ACTION_UP, ACTION_DOWN, ACTION_MOVE和ACTION_CANCEL等。

  1. ACTION_DOWN
case MotionEvent.ACTION_DOWN:
setPressed(true, x, y);
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,  y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);

在ACTION_DOWN事件处理中,setPress用于指示View对象是否进入press状态。这样View就可以设计不同press状态下的差别显示(比如Button在press与normal状态下的背景不同)。另外当收到DOWN事件后,View就开始检测它会不会演变成长按事件,可以通过ViewConfiguration.getLongPressTimeout()获取长按时间,在Timeout后系统就需要进行长按处理。如果用户通过setOnLongClickListener设置了响应的函数,那么就会回调这些函数。

  1. ACTION_MOVE
    在按下后并拖动,随后就会产生ACTION_MOVE事件。这个事件随着用户的拖动会不断产生,直到ACTION_UP或者ACTION_CANCEL。
if (!pointInView(x, y, touchSlop)) {
 // Outside button
 // Remove any future long press/tap checks
 removeTapCallback();
 removeLongPressCallback();
 if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
    setPressed(false);
  }
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}

pointInView用于判断当前手势是否已经超出了view的范围,如果是,就会移除长按监听的操作并且View对象将退出press状态。

  1. ACTION_UP

手势操作的结束点,除了改变view的一系列状态,最重要的操作就是判断view受否设置了setOnClickListener所要响应的事件。

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    // This is a tap, so remove the longpress check
    removeLongPressCallback();
   // Only perform take click actions if we were in the pressed state
   if (!focusTaken) {
     // Use a Runnable and post this rather than calling
     // performClick directly. This lets other visual state
     // of the view update before click actions start.
     if (mPerformClick == null) {
         mPerformClick = new PerformClick();
      }
      if (!post(mPerformClick)) {
        performClickInternal();
        }
     }
}

performClick并不会马上调用执行,而是通过post队列排队的方式去处理。这样做可以让其他View状态优先得到更新处理,以保证执行click操作时这些状态时正确的。

4.ACTION_CANCEL

ACTION_CANCEL比较特殊,并不有用户主动产生,而是有系统谨慎判断后得出结果。这个事件说明当前手势已经被废弃,后续不会有任何和该手势相关的事件产生。

 case MotionEvent.ACTION_CANCEL: 
      if (clickable) {  
        setPressed(false);  
      }
      removeTapCallback();                     removeLongPressCallback();                     mInContextButtonPress = false;                     mHasPerformedLongPress = false;                     mIgnoreNextUpEvent = false;   
      mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;               
      break;

二、ViewGroup中TouchEvent的投递流程

ViewGroup与View在接收事件的流程上基本一致,因为ViewGroup要对子对象进行处理,具体实现为ViewGroup重载View中的dispatchTouchEvent方法,对View提供的派发机制进行重新规划。

public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;//event是否被处理
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            /*Step1*/ 
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            /*Step2*/ 
            // 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;
            }    
}

step1: Down事件是后续事件的起点,所以一旦收到ACTION_DOWN程序就会清理以前的状态cancelAndClearTouchTargets(ev)和resetTouchState();

step2: 变量intercepted代表是否拦截事件。

  1. actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null

如果是DOWN事件,或者mFirstTouchTarget不为空,分为两种情况

1.1、 disallowIntercept 为false

此时ViewGroup允许拦截,就需要通过intercepted =onInterceptTouchEvent(ev)来判断是否要真正执行拦截。

1.2、disallowIntercept 为true

ViewGroup不允许拦截

2.如果不是DOWN事件,而且mFirstTouchTarget为空,那么intercepted为true,表示ViewGroup选择继续拦截事件。

step3:如果intercepted为false,表明ViewGroup不希望拦截这一消息

if (actionMasked == MotionEvent.ACTION_DOWN
    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
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);
        
       if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
         ev.setTargetAccessibilityFocus(false);
         continue;
        }
       newTouchTarget = getTouchTarget(child);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            ...
        }
    }
}

执行归属判断的是canReceivePointerEvents和isTransformedTouchPointInView。前者表示这个child是否能接受Pointer Events;后者计算(x, y)这个点是否落在child的范围内。如果找到了事件的归属者,接下来就将事件投递给它,实现函数为dispatchTransformedTouchEvent。如果child != null这个函数会调用child的dispatchTouchEvent。如果child == null 就会有当前ViewGroup处理,调用super.dispatchTouchEvent()由于ViewGroup也是View。

总结:

消息的处理过程的理解

ViewGroup直接拦截intercept消息,起决定作用的是onInterceptTouchEvent。Android建议继承ViewGroup时只重载onInterceptTouchEvent,而不是重载dispatchTouchEvent。因为后者是所有ViewGroup共性的提取,不应轻易改变;前者是体现所有ViewGroup差异的地方。

  • 当onInterceptTouchEvent返回false时,说明当前的ViewGroup并没有拦截这个事件,所以它需要继续往下传递,知道找到处理的View.

  • 当onInterceptTouchEvent返回true时,说明当前ViewGroup需要拦截这个事件以供内部处理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章