引言:
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 将拦截事件,自己通过相应的方法消费掉事件。
四、结论
- 事件确实由父布局向下传递,最早由 ViewGroup 接收,由 ViewGroup 交给 View
- ViewGroup 默认不拦截事件,将事件交给 View 处理,但如果我们重写 onIterceptTouchEvent 方法,则可以拦截事件,让 ViewGroup 的相应方法处理点击事件
- 若 View 将事件消费掉,ViewGroup中将无法接收到任何事件,换言之,若 View 处理事件后未将事件消费,ViewGroup 也能接收到点击事件进行处理。