Android事件分发机制

简介: 说在开头,之前项目中使用到了ListView和Button的组合,由于两者都有click事件,也意识到应该是Android的事件分发机制的原因。面试时也特意去恶补过,不过也是一知半解,此次因在项目中遇到该问题特意去详细了解一下。

说在开头,之前项目中使用到了ListView和Button的组合,由于两者都有click事件,也意识到应该是Android的事件分发机制的原因。面试时也特意去恶补过,不过也是一知半解,此次因在项目中遇到该问题特意去详细了解一下。

引言

点击事件的分发机制由于主要发生在界面中,需要先了解Android系统的UI架构,如下图所示。


我们都知道Android程序的UI是由Activity这个组件构成的,而实际中是使用setContentView这个方法设置一个自定义布局的,这里的ContentView就是存放这个自定义布局的。而ContentView和TitleView组成了顶级View,即DecorView,这样就可以看成Activity-Window-View的关系。一个Activity包含一个Window,而Window类是一个抽象类,PhoneWindow实现了该类,PhoneWindow类将一个DecorView设置为应用窗口的根View。点击事件就是从Activity开始,通过PhoneWindow传递到DecorView中。这个分发的流程可以认为一个点击事件一层一层的传递,这一层级不去需要就传递给子层去消费(custom),消费的话告知父层已经消费,没有消费同样告知父层然后父层去是否消费这个事件。

Android点击事件分发流程

Activity首先获取UI的点击事件,点击事件通过dispatchTouchEvent方法继续分发,该方法的源码如下:
    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
这个方法是个布尔类型的方法,如果事件被消费就返回true,getWindow().superDispatchTouch(ev)是调用的Window类中的 superDispatchTouch方法,到此Activity将点击事件传递给Window中。在引言中说过,Window类是个抽象类本身不能实例化,是由PhoneWindow类来实现的,不过我们可以看一眼Window类(主要就是官方的解释),省略掉其他方法。
/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.policy.PhoneWindow, which you should instantiate when needing a
 * Window.  Eventually that class will be refactored and a factory method
 * added for creating Window instances without knowing about a particular
 * implementation.
 */
public abstract class Window {
......
   /**
     * Used by custom windows, such as Dialog, to pass the key shortcut press event
     * further down the view hierarchy. Application developers should
     * not need to implement or call this.
     *
     */
    public abstract boolean superDispatchTouchEvent(MotionEvent event);

......
}
类的官方说明中说到仅有的实现这个抽象类的就是android.policy.PhoneWindow类,而superDispatchEvent方法也是个抽象布尔类型的方法,将事件传递到view层,特别明确说到程序开发者不需要实现或者调用这个方法。既然是PhoneWindow类实现的这个方法,下面就要转到PhoneWindow类中。实现代码只有一句:
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
mDecor是DecorView的一个实例,可以看到PhoneWindow将事件分发到DecorView(一个final类),至此点击事件终于来到了根View中。而DecorView中包括了ContentView,一般ContentView就是我们常用到的View,而它往往是一个ViewGroup(如LinearLayout),可以直接看ViewGroup中对点击事件的处理,由于实现代码太多,这里只摘取部分关键代码做解释用。
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }
        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;
            }

            ......

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

        }

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

截取的代码中第二个if中做了两个事情,第一个是检查是否需要拦截,如果在这一层需要拦截消耗,如果onInterceptTouchEvent返回true,说明要拦截这个事件,随后会调用onTouchEvent方法去消费这个事件,这里要注意ViewGroup是没有onTouchEvent方法的,这个方法存在于View中,ViewGroup中是默认不拦截事件的,会先分发到子View中进行消费。第二个方法调用了ViewGroup对点击事件处理的方法dispatchTransformedTouchEvent。篇幅影响就先不看这个类了(其实也没怎么看懂。。。)

最后来看View的dispatchTouchEvent方法
/**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            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;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }
在这里dispatchTouchEvent首先会调用onTouch方法,当然如果没有OnTouchListener就会直接调用onTouchEvent。如果dispatchTouchEvent或者onTouchEvent返回true,证明点击事件被消费,不再往子View中分发;而如果onTouchEvent返回false,则点击事件又传递给父View,由父View去消费以此类推,直到ViewGroup。如果ViewGroup也无法处理,就会调用Activity的onTouchEvent方法来消费这个点击事件了。

开头的案例

开头说过遇到的ListView和Button的点击事件冲突问题,其实在布局文件中添加两行代码即可,在Button的属性中添加
android:focusable="false"
而在ListView所在布局文件的根布局中,如顶层的LinearLayout中添加
android:descendantFocusability="blocksDescendants"
即可
这样可以即实现Button的OnClick方法,也可以使用ListView的OnItemClick方法了。

写在最后

很久没有写博客了,尤其是稍微有点技术含量的就更少了,不足之处还是有很多的,希望能继续完善自己的技能了和写作的方式。写这篇文章也借鉴了不少网络上的资源,这里用的Android源码是5.0的,没有用到比较新的6.x和7.x,不过这个模块应该都差不多。





目录
相关文章
|
4月前
|
Android开发 容器
[Android]View的事件分发机制(源码解析)
[Android]View的事件分发机制(源码解析)
36 0
|
7月前
|
Android开发 开发者
Android View 事件分发机制,看这一篇就够了(二)
Android View 事件分发机制,看这一篇就够了
|
7月前
|
Android开发
Android View 事件分发机制,看这一篇就够了(一)
Android View 事件分发机制,看这一篇就够了
|
10月前
|
Android开发 开发者 容器
Android事件分发机制
Android事件分发机制
|
监控 Android开发 索引
“framework必会”系列:Android Input系统(二)事件分发机制
对于目前应用开发已经饱和的大环境下,作为一个多年Android开发,逼迫我们Android开发往更深层次的framework层走,于是就有了这么个系列
|
调度 Android开发
Android 事件分发机制详解(下)
Android 事件分发机制详解(下)
Android 事件分发机制详解(下)
|
Android开发 开发者
Android 事件分发机制详解(上)
Android 事件分发机制详解(上)
Android 事件分发机制详解(上)
|
程序员 Android开发
牛逼!终于有人能把Android事件分发机制讲明白了
在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少。废话不多说,总结一句:事件分发机制很重要。
牛逼!终于有人能把Android事件分发机制讲明白了
|
Android开发
Android事件分发机制之ACTION_DOWN
Android的事件分发机制也是老生常谈了,这篇文章并不是笼统的介绍这个机制,而是针对ACTION_DOWN这个事件探讨相关的细节。
319 0
|
Android开发 容器
事件分发四部曲之一《深度遍历讲解Android事件分发机制》
事件分发四部曲之一《深度遍历讲解Android事件分发机制》
事件分发四部曲之一《深度遍历讲解Android事件分发机制》