Android布局中同级View的事件传递优先级
个人原创,转载请注明出处:https://www.jianshu.com/p/10a2d2304f1e
说起Android中View
的事件分发机制,不少开发者脑海中应该会立刻浮现出一幅流程图。已经有许多文章详细的分析了点击事件在上下级View
和ViewGroup
之间的传递规则。但同级View之间的点击事件是如何专递的呢?换句话说,处于同一个ViewGroup
内的两个View
重合时,ViewGroup
是如何决定传递到哪一个View
的?部分有经验的开发者可能会说:按照xml
中的排列顺序,最后的优先触发。的确,在相当长的时间里我也是这么认为的。但在最近的开发中我遇到了一个比较棘手的问题,这也促使我从源码中去进行更深入的探索。
决定事件传递对象的源码分析
点击事件的分发机制主要由dispatchTouchEvent()
,onInterceptTouchEvent()
和onTouchEvent()
三个方法来完成,其中后两个方法都是在第一个方法中调用的,作用分别是拦截事件和处理事件,与本文关系不大。那么,决定父控件将点击事件传递给哪个子控件的逻辑,就应该在dispatchTouchEvent()
剩余的代码里。通常dispatchTouchEvent()
这个方法不太可能会被重写,因此我们直接看ViewGroup
的dispatchTouchEvent()
方法:
...
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 = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
...
}
}
...
整段方法的代码好长啊,足足有200+行!不过我们要善于抓住核心。在方法体内搜索child
关键字能定位到上图所示的一段代码。通过行间的注释我们可以得知判断父控件将点击事件传递给哪个子控件的逻辑就在这段代码中。而决定这个子控件的实例的代码应该就是最后一行:
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
很显然getAndVerifyPreorderedView()
这个方法决定了最终由哪个子控件来接收点击事件。方法的具体逻辑我们先放到一边,先来看看该方法接收的3个变量,其中children
根据上方代码可推测出是包含了所有(重合)子控件的实例数组,而另外两个变量preorderedList
和childIndex
从变量名就能猜到和顺序有关。我们先来看看决定preorderedList
的buildTouchDispatchChildList()
方法,该方法直接调用了buildOrderedChildList()
方法,我们继续看该方法的代码:
ArrayList<View> buildOrderedChildList() {
final int childrenCount = mChildrenCount;
if (childrenCount <= 1 || !hasChildWithZ()) return null;
if (mPreSortedChildren == null) {
mPreSortedChildren = new ArrayList<>(childrenCount);
} else {
// callers should clear, so clear shouldn't be necessary, but for safety...
mPreSortedChildren.clear();
mPreSortedChildren.ensureCapacity(childrenCount);
}
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
// add next child (in child order) to end of list
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View nextChild = mChildren[childIndex];
final float currentZ = nextChild.getZ();
// insert ahead of any Views with greater Z
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
又是好长的一段代码哟!不过眼尖的应该很快就能注意到第二行
if (childrenCount <= 1 || !hasChildWithZ()) return null;
这个判断语句,childrenCount
很明显就是子控件的数量,如果小于等于1就不用判断了。而hasChildWithZ()
熟悉布局文件的开发者应该能猜到这是查看是否有child
设置了Z轴相关属性,取反意味着如果没有child
设置Z轴就返回null
。其实搞清楚这里基本上下面的一大段代码就不用看了!根据实际经验很容易得出这段代码就是让Z轴越大的优先级越高!
接着再来看childIndex
,进入方法getAndVerifyPreorderedIndex()
中:
private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
final int childIndex;
if (customOrder) {
final int childIndex1 = getChildDrawingOrder(childrenCount, i);
if (childIndex1 >= childrenCount) {
throw new IndexOutOfBoundsException("getChildDrawingOrder() "
+ "returned invalid index " + childIndex1
+ " (child count is " + childrenCount + ")");
}
childIndex = childIndex1;
} else {
childIndex = i;
}
return childIndex;
}
这段代码的逻辑比较简单,变量customOrder
顾名思义就是自定义顺序,如果为false
就是childIndex
取默认顺序,而默认顺序一般来讲就是xml
中子控件的定义顺序了。其实看到这里整个判断的逻辑已经比较清晰明了了,主要的影响因素就是Z轴大小和xml中的定义顺序。
最后我们再回过头来看给child
最终赋值的getAndVerifyPreorderedView()
方法:
private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,
int childIndex) {
final View child;
if (preorderedList != null) {
child = preorderedList.get(childIndex);
if (child == null) {
throw new RuntimeException("Invalid preorderedList contained null child at index "
+ childIndex);
}
} else {
child = children[childIndex];
}
return child;
}
这时再看这段代码就很明显了,preorderedList
不为null
时优先看preorderedList
,否则直接看childIndex
,即设置了Z轴就Z轴大的优先,否则就是xml定义靠后的优先。到这里,决定父控件将点击事件传递给哪个子控件的逻辑已基本清晰。
事件在子控件间的传递
一般来说不用考虑这个问题,因为View
的onTouchEvent()
默认会消耗事件,除非它是不可点击的,即View
的clickable
和longClickable
属性都为false
。当优先级最高的子View
不可点击时,事件会传递到次高的View
上,以此类推。
总结
当父布局下有两个重合的子控件A和B时,点击事件的传递遵循:
- 如果子控件设置了Z轴(
elevation
或translationZ
),就Z轴大的优先。 - 如果没有设置Z轴或Z轴相同,则
xml
中定义靠后的优先。 - 当优先级最高的子控件为不可点击(
clickable
和longClickable
属性都为false
)时,事件会传递到优先级次高的控件上,否则会默认消耗掉事件。