Android事件分发与传递机制(源码研究)

Android的时间分发与传递是个老生常谈的话题,面试中无数次被问,好多技术博客对此有或多或少的分析,但是别人的终究是别人的,而且好多分析都是基于Android之前的版本(本文是Android-26源码)。所以在此自己看下源码是怎么实现这种设计的,废话不多说。

Read the fucking source code。

那么问题来了,源码好几万行,当然不能一行一行的看,要带着问题与线索,捡关键的看。

问题一、setOnClickListener与setOnTouchListener的关系?

有点Android基础的都知道setOnTouchListener 中onTouch返回true的话setOnClickListener.onClick的监听将不再被调用。

分析原因:
进入View源码找到OnClickListener.onClick的调用位置是在performClick()方法中。
(注:ListenerInfo 是一个保存所以监听接口的类,所有的设置监听都放到这里面,源码里很容易理解)

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        ……

        return result;
    }

performClick()是在onTouchEvent()中的ACTION_UP中调用 大概在第12987行

 public boolean onTouchEvent(MotionEvent event) {
      ……

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                  ……

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                          ……
                            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)) {
                                    performClick();
                                }
                            }
                        }

所以OnClickListener.onClick最终是在onTouchEvent()中调用。
再来看View的dispatchTouchEvent()方法:

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

这里面代码不是很多,与本问题无关代码已省略,在此只关注这些就可以了。

  • 第一个if语句先判断了监听事件不为空;
  • 然后第二个(mViewFlags & ENABLED_MASK) == ENABLED,没深研究,viewFlag与ENABLED掩码做个与运算,猜测是判断能不能点击,大差不差。
  • 重点来了第三判断为 li.mOnTouchListener.onTouch(this, event) 方法的返回值。也就是我们onTouch的返回值。

然后结合下面 if 语句的判断,问题答案已经出现。当我们的onTouch返回true 时result被赋为true,于是就不会调用到onTouchEvent(),也就不会调用到OnClickListener.onClick。

问题二、onInterceptTouchEvent()与requestDisallowInterceptTouchEvent如何工作?

onInterceptTouchEvent()返回true的话事件将直接交给此ViewGroup的onTouchEvent处理。
子View可以通过getParent.requestDisallowInterceptTouchEvent(),控制父布局是否执行onInterceptTouchEvent。

为什么呢 往下看

Touch事件的起点的Activity的dispatchTouchEvent

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

然后调用Window的superDispatchTouchEvent,window是抽象类 只有一个实现类PhoneWindow,所以调用PhoneWindow:

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

最终调用DecorView,而DecorView继承Framelayout,所以最终调用ViewGroup的dispatchTouchEvent()。
这些好像都是废话!!!!捂脸
重点来了ViewGroup的dispatchTouchEvent():

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
      ……
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
          ……
            // 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;
            }

            ……

            if (!canceled && !intercepted) {

              ……
                  ……
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
               ……
            }

           ……
        return handled;
    }

代码灰常长,国际惯例,只展示针对此问题的部分,分析:
(Android-26源码)

  • 首先会再大概在2499行初始化一个intercepted 字段。

  • 然后2502行 会有一个disallowIntercept,可通过 ViewParent的requestDisallowInterceptTouchEvent改变此值。下面的代码很明显disallowIntercept将会影响到intercepted 会赋值为 onInterceptTouchEvent(ev)还是直接false。这里就是requestDisallowInterceptTouchEvent赋值为True将不会调用onInterceptTouchEvent的原因。

  • onInterceptTouchEvent如果返回true(代表要拦截此事件),那么代码将不会进入2529行的if (!canceled && !intercepted) 这个判断(透露一下,事件的分发是在这个判断里面继续的。所以不会往下分发了,后面会有分析)。而是会进入2536行的dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS)这里注意其三个参数为null;

看dispatchTransformedTouchEvent源码:

 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
           final boolean handled;
           ……

        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            ……
        }

        // Done.
        transformedEvent.recycle();
        return handled;
}

上面说了第三个参数也就是chlid为null。所以会调用super.dispatchTouchEvent。super很明显就是View,进而调用onTouchEvent 。于是就进入了问题一 中的分析。

所以结论是 很明显如果ViewGroup的onInterceptTouchEvent与requestDisallowInterceptTouchEvent会影响到ViewGroup会不会继续进行分发(就是会不会进入if (!canceled && !intercepted)中)而是直接继续往下执行最终调用父类View的dispatchTouchEvent从而最终执行到onTouchEvent。

问题三、ViewGroup如何向子View传递ACTION_DOWN事件,子View如何判断点击区域是不是自己?

继续看ViewGroup的dispatchTouchEvent,由问题二我们可知如果父布局不拦截事件,将会进入下面判断语句:

public boolean dispatchTouchEvent(MotionEvent ev) {
      ……
      TouchTarget newTouchTarget = null;
      if (!canceled && !intercepted) {
                ……

                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 preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                           ……

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                           ……
                        }
                        ……
                    }

                    ……
                }
        }
……
}
  • 每次分发开始newTouchTarget 都会赋值为null,这个的作用后面再说。
  • 如果然后得到子view的数量,如果childrenCount !=0则进入判断if (newTouchTarget == null && childrenCount != 0),这里面便是分发的逻辑。
  • 首先buildTouchDispatchChildList()方法会对子view进行一个排序,(排序逻辑有兴趣可以研究下)。
  • 然后循环所有的child, canViewReceivePointerEvents()方法判断child是否接受点击(就是判断下VISIBILITY或者是不是处于动画中);
  • 还有一个!isTransformedTouchPointInView(x, y, child, null)判断,这个就是问题的答案之一,点进去方法里面调用了View的pointInView()方法判断点击区域是不是属于自己。
  • 此时newTouchTarget 依然为空 于是来到 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))判断,此方法问题二中调用过,第三个参数为null,但这里不再是null了而是child,于是:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

       ……

        // Perform any necessary transformations and dispatch.
        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);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }
  • 最终调用了 child.dispatchTouchEvent,并将返回值赋给handled返回。至此事件传到子View中。

那么问题又来了,重叠的子View如和接受并禁止往下继续遍历传递呢?

  • 答案跟newTouchTarget 有关 ,TouchTarget 为一个单向链表,在这里可以理解为要处理事件的子view。
  • 如果子View消费此事件child.dispatchTouchEvent将返回true,最终dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign 也为true,则判断成立 将会在newTouchTarget = addTouchTarget(child, idBitsToAssign);为newTouchTarget 赋值。代表着此view就是处理事件的view,下次循环if (newTouchTarget != null)将会直接break。

问题四、为什么子View不处理ACTION_DOWN的话,将不会收到MOVE 或UP

哎呀好难解释,无非就是些判断,回头再写

总结:

每看一遍都有一遍的收获,其实就是一层一层的递归调用,网上说的什么U型传递并不恰当, 好佩服Google的工程师,期待我也能达到这个水平。

你可能感兴趣的:(Android事件分发与传递机制(源码研究))