Android面试复习之View事件体系(源码分析)

前言

昨天面试了腾讯Android,基本上是照着简历问,但都问的比较深入。其中问到了事件体系,含含糊糊的答了出来(之前有看过艺术探索),但后来自己想想感觉自己答的并不是特别好。虽然面试结果还不知道,但觉得还是应该好好整理一下。

分析的起点

不管是书上还是网上都说事件的起点是ViewGroup的dispatchEvent,但大多数都没有给出理由,本着探索的精神,我采用了最简单的方法:断点调试。

image.png
点击这个View,果然,查看栈帧:
image.png
是通过WindowCallback传递到Activity,再专递到Activity的Window->DecorView,DecorView实际上就是一个FrameLayout,最终调用的就是ViewGroup的dispatchTouchEvent,所以下面就可以愉快的分析DispatchEvent啦。

ViewGroup#dispatchTouchEvent()

            // 前面省略...
            final int action = ev.getAction();
            // 获取事件类型
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // ACTION_DOWN就是你手机接触屏幕的事件,通常被认为是一系列触摸事件的起点
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 这里是重置当前的事件状态,后面会分析
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 检查是否拦截
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // 这个标志如果有效,则不会调用自己的onInterceptTouchEvent方法
                // 可以通过ViewParent#requestDisallowInterceptTouchEvent()修改
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    // 如果intercepted为true,就会拦截这一系列事件,具体可以在后面的源码到
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); 
                } else {
                    intercepted = false;
                }
            } else {
                // 触摸事件不是ACTION_DOWN,并且touchTarget==null
                intercepted = true;
            }
复制代码

可以看到,是否拦截的逻辑还与touchTarget这个成员相关,这个成员是什么呢?

private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin;
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // The touched child view.
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        public TouchTarget next;
复制代码

看一下这个类的结构,很容易想到,这是个链表节点的结构,而它的child是什么呢?可以通过后面的代码去挖掘,因为mFirstTouchTarget这个成员变量是在后面赋值的,初始为null,所以我们可以把它认为是null,带着这个条件去走下面的逻辑。 按照ViewGroup的默认情况,不拦截事件,这个先看intercept为false的情况。以下是不拦截本次事件的时候会执行的一段代码。

// 首先会判断是不是ACTION_DOWN或者支持多指时是不是其他手机按下或者是鼠标按下(Hover是跟鼠标相关的处理,这里不用过多关心)
if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // 获取按下的手指编号,暂时不用关心
                    final int actionIndex = ev.getActionIndex(); 
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // 获取子View的列表,顺序可以通过ViewGroup提供的接口自定义
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        // 按一定顺序遍历子View
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            ...
                            // 判断这个View是否接收事件,并判断事件是否在View对应的那块矩形内,如果不在,找下一个
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            
                            // 这个方法实际上是遍历mFirstTouchTarget这个链表,找到child域和当前View相同的TouchTarget,但第一次收到down时,这个会返回null
                            newTouchTarget = getTouchTarget(child);
                            // 如果找到了,会把touchTarget响应的手指编号信息更新
                            if (newTouchTarget != null) 
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            resetCancelNextUpFlag(child);
                            // 分发事件,如果成功处理,更新事件处理的信息并退出循环,这里是把事件交给child去分发,具体如何实现这里不展开,逻辑比较简单
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    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;
                            }

                           
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    ...
复制代码

这里的代码就比较长了,但也不是很难懂,重要的地方都在注释。我们这里暂时是以第一次点击事件来描述这个流程的,因此去掉了一些与这个流程无关的代码。这段代码实际上对一些特殊情况进行了处理,这里咱们先略过。后面虽然还有很多代码,但实际上会发现,执行到这个地方,基本就结束了,alreadyDispatchedToNewTouchTarget被置为了true,带入源码读,可以看到

                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        ...
                    }
                    predecessor = target;
                    target = next;
                }
            }
复制代码

对于这个流程来讲,else分支已经不重要了,到此DOWN事件处理完毕。 当然,我们现在可以回过头看前面的问题。DOWN事件分发到的时候到底做了什么呢? 首先是cancelAndClearTouchTargets方法

private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }
复制代码

ViewGroup#clearTouchTargets

// 清空链表
private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
复制代码

可以看到,在这里会分发event,但是即使event不为null,传给dispatchTransformedTouchEvent的cancel的值为true,在这个方法处理的时候,会把event的Action设为ACTION_CANCEL,所以我们在处理ACTION_CANCEL的时候,一般要把事件相关的状态和变量重置。 接下来会调用ViewGroup#resetTouchState

private void resetTouchState() {
        // 清空链表
        clearTouchTargets();
        // 重置状态
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
复制代码

这里我们能看到,它会把FLAG_DISALLOW_INTERCEPT这个标志设置为false,也就是说,它这个时候会调用自己的interceptTouchEvent方法。由此我们得出一条结论: ACTION_DOWN事件不能被取消拦截 假设我们按下来,移动手指,这样就会产生一个move事件,这里,仍然假设默认不会拦截。

if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) 
...
复制代码

这一串代码自然不会执行,到了下面

            if (mFirstTouchTarget == null) {  
               // 暂不关心
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    // alreadyDispatchedToNewTouchTarget这个时候是false,会执行else分支
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
复制代码

这个时候,会遍历touchTarget这个链表并分发事件,从源码中可以看出,只要又一个touchTarget的child成功处理这个事件,handled就是true。 这里又有疑问了,为什么touchTarget会用链表来存?会有多个touchTarget的情况吗?这个时候,就要想到之前分析忽略的地方,对多指的支持。首先还是看上面那一长串代码的进入条件:

if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE)
...
复制代码

还有一个ACTION_POINTER_DOWN条件。什么是POINTER_DOWN呢?首先DOWN是指你第一个手指触摸屏幕,然后你第一根手指不放,按下第二根手指、第三根手指都会产生这样的事件,并且,还会记录手指的id。 可以看到上面的split这个变量,这是一个flag,当关心多指时为true(默认true)。接下来,获取本次pointerId:

final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
复制代码

idBitsToAssign实际上就是把手指id那一位置1的数。 接下来,会把之前处理过这个手指id的touchTarget清除。

private void removePointersFromTouchTargets(int pointerIdBits) {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            // 处理了这个手指的事件
            if ((target.pointerIdBits & pointerIdBits) != 0) {
                // 把这个手指对应的位置0
                target.pointerIdBits &= ~pointerIdBits;
                // 置0后没有对应处理的手指id了,则从链表中删除
                if (target.pointerIdBits == 0) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
复制代码

也不难理解为什么需要清除前面的,这个方法是为了同步状态,前面的手指,因为对于当前手指来说,相当于新开始一个DOWN事件,所以前面不应该有处理这个事件的touchTarget,这样做也是为了保险,可见Google大佬思维的严密。接下来的就有三种情况了:

  • 情况一:
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
复制代码

在遍历的时候,首先遍历了已经在touchTarget中的child,这个时候显然没有增加新的touchTarget,而是把它的处理的手指对应位置一。而之后的流程如前面分析,遍历touchTarget,分发事件。

  • 情况二: 先遇到了一个没有在链表中的结点,就会像前面处理DOWN事件那样添加到链表中。之后的处理也类似。
  • 情况三:
                  if (newTouchTarget == null && mFirstTouchTarget != null) {
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
复制代码

遍历完子View都没有找到,这时候把链表最后一个(最近添加的)手指信息对应位置1。 事实上,个人认为比较常见的是情况一。情况二、情况三的话需要改变遍历顺序或者移除上一次处理过的View。 上面是intercept=false的情况。那如果intercept=true呢?这个就比较简单了。

if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }
复制代码

会把dispatchTransformedTouchEvent的child参数设置为null,如果为null,会把事件交给super.dispatchTouchEvent。super是谁?可别忘了ViewGroup的爸爸是View!View又有自己的dispatchTouchEvent方法,这个方法就相对来讲比较简单了,主要是touchEventListener、click、longClick等的处理。

结语

当然,这个方法里还有一些细节的处理我没有分析,比如上面那段代码的canceled变量、accessibilityFocus的处理等。这里先埋个坑,之后有空回来补。 如果有什么分析错误的地方,欢迎各位大神指正!

猜你喜欢

转载自juejin.im/post/5c8223706fb9a049a62d6151
今日推荐