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,不过这个模块应该都差不多。





目录
相关文章
|
7天前
|
存储 安全 Android开发
探索Android与iOS的隐私保护机制
在数字化时代,移动设备已成为我们生活的一部分,而隐私安全是用户最为关注的问题之一。本文将深入探讨Android和iOS两大主流操作系统在隐私保护方面的策略和实现方式,分析它们各自的优势和不足,以及如何更好地保护用户的隐私。
|
1月前
|
消息中间件 存储 Java
Android消息处理机制(Handler+Looper+Message+MessageQueue)
Android消息处理机制(Handler+Looper+Message+MessageQueue)
40 2
|
25天前
|
Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
|
25天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
29天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
48 1
|
29天前
|
Android开发
Android面试高频知识点(1) 图解 Android 事件分发机制
Android面试高频知识点(1) 图解 Android 事件分发机制
38 1
|
30天前
|
存储 安全 数据安全/隐私保护
探索安卓与iOS的隐私保护机制####
【10月更文挑战第15天】 本文深入剖析了安卓和iOS两大操作系统在隐私保护方面的策略与技术实现,旨在揭示两者如何通过不同的技术手段来保障用户数据的安全与隐私。文章将逐一探讨各自的隐私控制功能、加密措施以及用户权限管理,为读者提供一个全面而深入的理解。 ####
53 1
|
1月前
|
消息中间件 存储 Java
Android消息处理机制(Handler+Looper+Message+MessageQueue)
Android消息处理机制(Handler+Looper+Message+MessageQueue)
45 2
|
1月前
|
XML 前端开发 Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
|
1月前
|
Android开发
Android 事件分发机制详细解读
Android 事件分发机制详细解读
39 4