深入探索 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<View> 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 也能接收到点击事件进行处理。
时间: 2024-10-12 17:31:41

深入探索 ViewGroup 的事件传递机制的相关文章

Android ViewGroup 触摸事件传递机制

引言 上一篇博客我们学习了Android View 触摸事件传递机制,不了解的同学可以查看Android View 触摸事件传递机制.今天继续学习Android触摸事件传递机制,这篇博客将和大家一起探讨ViewGroup的触摸事件传递机制. 示例 示例代码如下: public class MainActivity extends ActionBarActivity { private String TAG = "MainActivity"; private MyViewGroup pa

Android事件传递机制详解及最新源码分析——ViewGroup篇

在上一篇<Android事件传递机制详解及最新源码分析--View篇>中,详细讲解了View事件的传递机制,没掌握或者掌握不扎实的小伙伴,强烈建议先阅读上一篇. 好了,废话还是少说,直奔主题,开始本篇的ViewGroup事件传递机制探索之旅. 依然从简单的Demo例子现象开始分析 新建安卓工程,首先自定义一个Button以及一个RelativeLayout,很简单,只是重写了主要与事件传递机制相关的方法,代码如下: 自定义WLButton类: 1 public class WLButton e

Android中事件传递机制的总结

事件传递虽然算不上某个单独的知识点,但是在实际项目开发中肯定会碰到,如果不明白其中的原理,那在设计各种滑动效果时就会感到很困惑. 关于事件的传递,我们可能会有以下疑问: 事件是如何传递的 事件是如何处理的 自定义view的时候,事件也冲突了怎么解决 带着这三个疑问,我们来总结一下事件传递机制是怎么回事. 一.事件分发的原理: 1.事件是如何传递的: (1)首先由Activity分发,分发给根View,也就是DecorView(DecorView为整个Window界面的最顶层View) (2)然后

【转】Android Touch事件传递机制解析

原文地址:http://www.cnblogs.com/runssnail/p/4250549.html 说明:本文在原文地址上有所改动 一.小故事 在讲正题之前我们讲一段有关任务传递的小故事,抛砖迎玉下: 话说一家软件公司,来一个任务,分派给了开发经理去完成 开发经理拿到,看了一下,感觉好简单,于是 开发经理:分派给了开发组长 开发组长:分派给了自己组员(程序员) 程序员:分派给了自己带的实习生. 实习生:好苦逼,无法分派,怎么办啊?只能自己干了 但是实习生能不能做好,有两种情况了. 情况一:

Android ViewGroup触摸屏事件派发机制详解与源码分析

PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! [工匠若水 http://blog.csdn.net/yanbober] 该篇承接上一篇<Android View触摸屏事件派发机制详解与源码分析>,阅读本篇之前建议先阅读. 1 背景 还记得前一篇<Android View触摸屏事件派发机制详解与源码分析>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事

Android Touch事件传递机制详解 上

尊重原创:http://blog.csdn.net/yuanzeyao/article/details/37961997 最近总是遇到关于Android Touch事件的问题,如:滑动冲突的问题,以前也花时间学习过Android Touch事件的传递机制,可以每次用起来的时候总是忘记了,索性自己总结一下写篇文章避免以后忘记了,其实网上关于Touch事件的传递的文章真的很多,但是很少有系统性的,都是写了一个简单的demo运行了一下,对于我们了解Android Touch事件基本上没有任何帮助. 今

android事件传递机制详解

本篇内容将结合Android源码来分析Android的事件传递机制.众所周知,点按.滑动.触摸构成了Android等智能设备的基本操作,几乎所有的应用都通过对触摸屏的操作来进行应用程序的使用.那么,在Android中,触摸事件是如何响应及传递的呢,通过本篇内容你将有一个初步的了解. 实验环境 OS X 10.9 Eclipse(ADT) Android源码版本:API Level 19(Android 4.4) Android事件构成 在Android中,事件主要包括点按.长按.拖拽.滑动等,点

Android事件传递机制(转)

Android事件构成 在Android中,事件主要包括点按.长按.拖拽.滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作.所有这些都构成了Android中的事件响应.总的来说,所有的事件都由如下三个部分作为基础: 按下(ACTION_DOWN) 移动(ACTION_MOVE) 抬起(ACTION_UP) 所有的操作事件首先必须执行的是按下操作(ACTIONDOWN),之后所有的操作都是以按下操作作为前提,当按下操作完成后,接下来可能是一段移动(ACTIONMOVE)然后抬起(ACTI

Android Touch事件传递机制引发的血案

尊重原创:http://blog.csdn.net/yuanzeyao/article/details/38942135 关于Android Touch事件传递机制我之前也写过两篇文章,自觉得对Touche事件还是理解得比較清楚的,可是近期遇到的一个问题,让我再次对Android Touche事件进行一次学习. 我的关于Android Touche事件传递机制的文章例如以下: http://blog.csdn.net/yuanzeyao/article/details/37961997 http