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