美文网首页
聊一聊Android中的事件传递

聊一聊Android中的事件传递

作者: 一个追寻者的故事 | 来源:发表于2020-10-31 18:40 被阅读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又会针对不同的事件类型完成不同的处理工作:

    • ACTION_DOWN:
      在每一个touch事件周期的 ACTION_DOWN 中都会向主线程的消息队列中发送一个消息,并且延迟500ms执行。这个消息具体干了什么事:
    if (performLongClick(mX, mY)) {    //如果消费了 LongClick事件
        mHasPerformedLongPress = true;    // 表示已经消费了LongClick事件
    }
    

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

    • ACTION_UP:
      如果 LongClick 长按事件还没有消费,则接下来会处理Click事件,调用mOnClickListener.onClick方法。同时会删除 ACTION_DOWN 中发送到主线程消息队列中的检测长按事件的消息。
      如果 LongClick 长按事件已经被消费,就不会执行 Click 事件了。

    一个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 的代码实现还是有点复杂,考虑了各种组合情况,调用逻辑采用递归的形式。大概分为下面几部分:

    • 检查是否拦截调用 onInterceptTouchEvent
    • 如果没有被拦截 且 不是ACTION_CANCEL事件,如果事件是 ACTION_DOWN则遍历ViewGroup自己的子View(因为ACTION_DOWN之后,要么没有消费对象,要么有View消费,这两种情况对于以后的事件都不需要再遍历了),调用子View的dispatchTouchEvent,利用递归实现同样逻辑。然后如果递归的期间发现有View消费了事件,则每一层都会保留下一层消费了事件的子View。
    • 最后一步进行事件分发(这里有个注意的地方,在执行ACTION_DOWN时,之前已经遍历分发过了,所以这里会做排除)。 mFirstTouchTarget 为空的,调用自己的 onTouchEvent。 不为空 mFirstTouchTarget 则直接通知其进行事件传递。

    相关文章

      网友评论

          本文标题:聊一聊Android中的事件传递

          本文链接:https://www.haomeiwen.com/subject/ocxlmktx.html