说在开头,之前项目中使用到了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,不过这个模块应该都差不多。