聊一聊Android中的事件传递

2020-10-31  本文已影响0人  一个追寻者的故事

文章中所有代码均是不完整的代码,旨在说明具体逻辑,具体详细代码需参考sdk

我们知道一个事件是首先会传递到Activity:

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

    public boolean onTouchEvent(MotionEvent event) {
        ...
        return false;
    }
}

Activity#dispatchTouchEvent :这个方法会被调用用于处理屏幕touch事件,MotionEvent代表了屏幕touch事件,返回值:如果事件被消费了返回 true,否则返回 false。 这个方法干了两件事:
1、把touch事件传递给Window去处理
2、如果事件上一步没有人消费(整个View数没有view去消费这个事件),则回调 Activity#onTouchEvent去处理。

Activity#onTouchEvent:当Activity中所有的View都不对此次事件进行处理时(事件没有被消费),会调用这个方法。返回值:如果Activity消费了事件则返回 true,否则返回 false,默认返回 false

getWindow().superDispatchTouchEvent(ev)会把事件传递给 WindowWindow会把事件传递给 DecorView,调用DecorView#dispatchTouchEventDecorView是View树的顶层View,它是一个 ViewGroup

小结:至此我们知道,一个touch事件来了以后会首先传递给View树顶层View:DecorView。如果整个View树都不对此次事件进行消费的话,那么会看Activity有没有想法想对事件进行消费。View树的组织形式采用了组合设计模式,树的节点元素是:ViewGroup 和 View。 接下来我们从代码上看一下 View 和 ViewGroup 是如何对 touch 事件进行响应的。

代码

代码部分:View 和 ViewGroup

View
public class View{
  public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;

    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;
    }
    return result;
  }
}

View#dispatchTouchEvent:把touch事件传递给目标View,自己本身也可能是目标View。返回值:如果touch事件被这个View消费了返回 true,否则返回 false。处理逻辑:
1、如果此View设定了onTouchListener,且其onTouch方法返回 true表示此View消费了事件。
2、如果View的onTouchEvent返回 true表示消费了事件。
其它情况则表示未消费此事件。

我们分析一下View 的 onTouchEvent方法:

public class View{
  public boolean onTouchEvent(MotionEvent event) {
    ...
    //是否可点击
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    if (clickable){
        switch (action) {
          case MotionEvent.ACTION_UP:
            ...
            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                // This is a tap, so remove the longpress check
                removeLongPressCallback();
                ...
                performClickInternal();  //调用mOnClickListener.onClick方法
            }
            break;
          case MotionEvent.ACTION_DOWN:
            ...
            checkForLongClick(
                ViewConfiguration.getLongPressTimeout(),
                x,
                y,
               TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
            break;
          case MotionEvent.ACTION_CANCEL:
            removeTapCallback();
            removeLongPressCallback();
            break;
          case MotionEvent.ACTION_MOVE:
            // Be lenient about moving outside of buttons
            if (!pointInView(x, y, touchSlop)) {
                 // Outside button
                 // Remove any future long press/tap checks
                 removeTapCallback();
                 removeLongPressCallback();
            }
            break;
        }
        return true;    // 注意返回
    }
    return false;   //注意返回
  }
}

View#onTouchEvent:如果判断此View是可点击的,则返回true,表示消费事件,否则返回false,表示不消费事件。(如果View设置过 OnClickListener 或者 OnLongClickListener 则自动设置为CLICKABLE 或 LONG_CLICKABLE)。当clickable为true时,表示此view即为目标View,会消费事件。当消费事件时,View又会针对不同的事件类型完成不同的处理工作:

if (performLongClick(mX, mY)) {    //如果消费了 LongClick事件
    mHasPerformedLongPress = true;    // 表示已经消费了LongClick事件
}

会调用performLongClick 中调用了 mOnLongClickListener.onLongClick(View.this);就是如果onLongClick返回true,则代表已经消费了LongClick 事件

一个touch事件具体是 Click事件 还是 LongClick 事件,在按下去(ACTION_DOWN)的时候是无法知道的。

ViewGroup

ViewGroup继承自View,重写了 View 的 dispatchTouchEvent,接下来看一下 ViewGroup#dispatchTouchEvent 的实现逻辑:

public class ViewGroup{
    // First touch target in the linked list of touch targets.
    @UnsupportedAppUsage
    private TouchTarget mFirstTouchTarget;  // 记录了是哪个子View消费了事件
    

    public boolean dispatchTouchEvent(MotionEvent ev) {
      ...
      boolean handled = false;
      
      // 检查是否打断
      final boolean intercepted;
      if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
          ...
          if (!disallowIntercept) {    // requestDisallowInterceptTouchEvent 设置了以后全局控制
            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.
          // 什么情况下直接走这里呢?当ACTION_DOWN没有View进行消费,最后mFirstTouchTarget 就null。    
          // 这时候有ACTION_UP来的时候就是这种情况,此时就会直接自己的 onTouchEvent,不会再往下传递了。 又或者 ACTION_DOWN  -> ACTION_MOVE 被拦截,之后 ACTION_UP 时也是这种情况。
           intercepted = true;
       }
  
       // Check for cancelation.
       final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;  // CANCEL事件
       
       // 没有取消 && 没有打断
       if (!canceled && !intercepted) {
            // ACTION_DOWN的时候才会去遍历所有子View寻找目标View
            if (actionMasked == MotionEvent.ACTION_DOWN
                        || ...) {
                // 遍历子View,查看是否有子View是否消费了此次事件
                for (int i = childrenCount - 1; i >= 0; i--) {
                     // 如果子view 不在事件的坐标范围,则遍历下一个子View
                     if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                          ev.setTargetAccessibilityFocus(false);
                          continue;
                      }
                      ...
                  
                      if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                             ...
                            break;
                      }
             }
       }
       
      // Dispatch to touch targets.   把事件分发给target View
      if (mFirstTouchTarget == null) {
          // No touch targets so treat this as an ordinary view.      把事件分发给自己。
          handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
      } else {
             TouchTarget target = mFirstTouchTarget;
              while (target != null) {
                    final TouchTarget next = target.next;
                    // 如果 是ACTION_DOWN的情况已经做过事件分发了,不会再处理了,直接返回true
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;  // ACTION_DOWN 做过事件分发了
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        // 已经确定了target view,直接把事件分配下去
                       // 如果mFirstTouchTarget存在,但是intercepted又是true,表示刚开始ACTION_DOWN时子View确定要消费且父View不拦截,可是后续有时间过来的时候比如 ACTION_MOVE,此时父View表示拦截,就会给子View 发送 ACTION_CANCEL事件
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            // 清空  mFirstTouchTarget
                        }
                        ...
                        }
                    }
                    target = next;
                }
      }
      return handled;
    }
    
    // 把事件传递给 child 子View,调用 child.dispatchTouchEvent
    // 如果 child 子View为空,则直接调用 ViewGroup super中定义的dispatchTouchEvent。
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
        final boolean handled;
        // 如果需要cancel 
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);  //该表事件类型
            if (child == null) {
                handled = super.dispatchTouchEvent(event); 
            } else {
                handled = child.dispatchTouchEvent(event);  // 通知子View父类接管了
            }
            event.setAction(oldAction);
            return handled;
        }
  
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            ...
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        return handled;
    }

   // 把确定消费此次事件的child包装成TouchTarget,然后记录在 ViewGroup 的mFirstTouchTarget属性中
   private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

ViewGroup#dispatchTouchEvent 重写了View的dispatchTouchEvent,实现了ViewGroup的事件分发逻辑。ViewGroup 中有一个属性 mFirstTouchTarget,在ACTION_DOWN事件分发的过程中如果事件最终被ViewA消费,那么ViewA 到 DecorView 的View 树中,经过事件传递的每一个ViewGroup 都会记录此次事件最终被自己的哪一个子View消费了,这个字段就是 mFirstTouchTarget,记录下来的目的是为了在接下来的 ACTION_MOVE 和 ACTION_DOWN 时直接可以找到目标View,直接把相应的事件分配给它们。这里有一个注意的点是:如果ACTION_DOWN事件来时,View树中的某一个ViewX确定要消费此次事件,此时 ViewX 的父容器 到 DecorView中 的mFirstTouchTarget 都不为空,可是后来,当ACTION_MOVE、ACTION_UP时,View树中的某一个ViewGroup表示对事件进行拦击,那么这个时候,就会根据 mFirstTouchTarget 回调下面的每一层View一个事件 ACTION_CANCEL,并且清空之后的每一层View中 mFirstTouchTarget 的值。

ViewGroup#dispatchTouchEvent 的代码实现还是有点复杂,考虑了各种组合情况,调用逻辑采用递归的形式。大概分为下面几部分:

上一篇下一篇

猜你喜欢

热点阅读