深入探索 ViewGroup 的事件传递机制

引言:

ListView 中嵌套按钮,按钮点击事件无效,将按钮 setFocusable(false) 后才能正常使用点击事件的情况相信许多开发者都遇到过,很多人可能找到一个解决办法就把这个问题抛在了一边,但题主本着探索的精神从源码的角度找到了问题的答案

如果没有看过 深入理解 View 的事件传递机制 ,看今天的内容可能会有些吃力,所以建议大家还是先去看看我的这篇文章啦。

废话不多说,进入正题:

一、概述 ViewGroup 事件传递机制

研究 ViewGroup 之前,我们不妨先想一想:ViewGroup 是什么?它和 View 有关系吗?如果它们之间存在关系,那么 View 的事件传递机制适用于 ViewGroup 吗?

一张经典的 View 与 ViewGroup 的关系图与老罗的 Android 源码情景分析里的一张图给了我们答案(Android应用程序窗口(Activity)的视图对象(View)的创建过程分析):

确实,ViewGroup 中包含的全都是 View,那么说明 View 的事件传递机制是适用于 ViewGroup 的。读过 深入理解 View 的事件传递机制 可能就奇怪了,既然两者的事件传递机制是一致的,那文章开头提到的“点击事件无效”的问题是怎么产生的?

因为 View 的事件由根布局逐级向下传递给父布局,由父布局传递给子控件,那么在文章开头提到的问题里,许多人会下意识地认为:点击事件应该由 ListView 中的 Item 布局先接收到事件,再分发给 Button,那么 onItemClick 应该是能够响应的。

事实上,View 的事件传递机制没有问题,大家对机制的理解也没有问题,那到底是什么原因导致了这个谜之Bug的产生呢?

二、分析日志

与 深入理解 View 的事件传递机制 一样,我们重写 ListView 和 Button 与点击相关的方法,打出相应的 Log 来进行分析,代码很简单,我简单贴一下:

    btn = (Button)convertView.findViewById(R.id.button);
                btn.setOnClickListener(new OnClickListener() {

                    @Override
                    public void onClick(View v) {
                        Log.v("tag", "btn-OnClick");
                    }
                });
    convertView.setOnClickListener(new OnClickListener() {

                @Override
                public void onClick(View v) {
                    Log.v("tag", "list-item-OnClick");
                }
            });
    @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            Log.v("tag", "MyListView-onInterceptTouchEvent");
            return super.onInterceptTouchEvent(ev);
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            Log.v("tag", "MyListView-dispatchTouchEvent");
            return super.dispatchTouchEvent(ev);
        }

我在这里没有在 onItemClick 方法里面打Log,但实际上我对 convertView 设置点击事件效果也是一样的(其实就是我懒 + 代码写的烂……),那打出来的 Log 是什么样的呢?

图里很明显的告诉我们,点击事件确实由 ListView 进行分发,但 ListView 的子 item 点击事件,也就是 convertView 的点击事件并没有被响应,反倒是 Button 的点击事件却被响应了,这就奇了个怪了,不是说好了 convertView 是 Button 的父布局,事件由父布局交给子空间吗,怎么变成现在这样了……我们按照常规套路,把 Button 的 setClickable 方法设为 false。再看看 Log

我真是 fck dog 了,点击事件又谜之出现了……

这到底是为什么呢?

三、分析源码

被神之 Bug 困扰的大家先莫慌,不妨跟我来读读源码放松一下,我们先看看 ListView:

ViewGroup -> AdapterView -> AbsListView -> ListView

哇噢,原来 ListView 是 ViewGroup 的子类啊,我们再来看看 ListView 的事件分发方法,但我们翻遍源码都找不到对应的 dispatchTouchEvent 和 onInterceptTouchEvent 方法,那只能向上从父类找了……

最后我们发现,ListView 的事件分发方法就是 ViewGroup 的事件分发方法:

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 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();
            }

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

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    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 = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(childIndex);
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                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;
                            }
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // 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 {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    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;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

天哪,代码这么多我们怎么看……莫慌,抱紧我,一起看看核心代码就好了:

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

我们可以看到,DispatchTouchEvent 方法先通过 disallowIntercept 判断是否拦截,假如拦截,就调用自身的 onTouchEvent,消费事件;假如未拦截,通过一个 for 循环找出被点击的 View,调用子 View 的 DispatchTouchEvent,若子 View 仍是 ViewGroup 则递归调用 DispatchTouchEvent 方法。

那我们不妨看看 ViewGroup 的 onIterceptTouchEvent 方法:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
            return false;
        }

我们可以发现,方法返回值恒为 false,换言之,ViewGroup 默认不拦截事件,将点击事件交由子 View 处理;如果子 View 不可点击,那么表示 ViewGroup 将拦截事件,自己通过相应的方法消费掉事件。

四、结论

  1. 事件确实由父布局向下传递,最早由 ViewGroup 接收,由 ViewGroup 交给 View

  2. ViewGroup 默认不拦截事件,将事件交给 View 处理,但如果我们重写 onIterceptTouchEvent 方法,则可以拦截事件,让 ViewGroup 的相应方法处理点击事件

  3. 若 View 将事件消费掉,ViewGroup中将无法接收到任何事件,换言之,若 View 处理事件后未将事件消费,ViewGroup 也能接收到点击事件进行处理。

你可能感兴趣的:(Android,机制)