Android的时间分发与传递是个老生常谈的话题,面试中无数次被问,好多技术博客对此有或多或少的分析,但是别人的终究是别人的,而且好多分析都是基于Android之前的版本(本文是Android-26源码)。所以在此自己看下源码是怎么实现这种设计的,废话不多说。
Read the fucking source code。
那么问题来了,源码好几万行,当然不能一行一行的看,要带着问题与线索,捡关键的看。
问题一、setOnClickListener与setOnTouchListener的关系?
有点Android基础的都知道setOnTouchListener 中onTouch返回true的话setOnClickListener.onClick的监听将不再被调用。
分析原因:
进入View源码找到OnClickListener.onClick的调用位置是在performClick()方法中。
(注:ListenerInfo 是一个保存所以监听接口的类,所有的设置监听都放到这里面,源码里很容易理解)
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
……
return result;
}
performClick()是在onTouchEvent()中的ACTION_UP中调用 大概在第12987行
public boolean onTouchEvent(MotionEvent event) {
……
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
……
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
……
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
所以OnClickListener.onClick最终是在onTouchEvent()中调用。
再来看View的dispatchTouchEvent()方法:
public boolean dispatchTouchEvent(MotionEvent event) {
……
boolean result = false;
……
……
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;
}
……
return result;
}
这里面代码不是很多,与本问题无关代码已省略,在此只关注这些就可以了。
- 第一个if语句先判断了监听事件不为空;
- 然后第二个(mViewFlags & ENABLED_MASK) == ENABLED,没深研究,viewFlag与ENABLED掩码做个与运算,猜测是判断能不能点击,大差不差。
- 重点来了第三判断为 li.mOnTouchListener.onTouch(this, event) 方法的返回值。也就是我们onTouch的返回值。
然后结合下面 if 语句的判断,问题答案已经出现。当我们的onTouch返回true 时result被赋为true,于是就不会调用到onTouchEvent(),也就不会调用到OnClickListener.onClick。
问题二、onInterceptTouchEvent()与requestDisallowInterceptTouchEvent如何工作?
onInterceptTouchEvent()返回true的话事件将直接交给此ViewGroup的onTouchEvent处理。
子View可以通过getParent.requestDisallowInterceptTouchEvent(),控制父布局是否执行onInterceptTouchEvent。
为什么呢 往下看
Touch事件的起点的Activity的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
然后调用Window的superDispatchTouchEvent,window是抽象类 只有一个实现类PhoneWindow,所以调用PhoneWindow:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
最终调用DecorView,而DecorView继承Framelayout,所以最终调用ViewGroup的dispatchTouchEvent()。
这些好像都是废话!!!!捂脸
重点来了ViewGroup的dispatchTouchEvent():
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
……
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;
}
……
if (!canceled && !intercepted) {
……
……
}
// 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 {
……
}
……
return handled;
}
代码灰常长,国际惯例,只展示针对此问题的部分,分析:
(Android-26源码)
首先会再大概在2499行初始化一个intercepted 字段。
然后2502行 会有一个disallowIntercept,可通过 ViewParent的requestDisallowInterceptTouchEvent改变此值。下面的代码很明显disallowIntercept将会影响到intercepted 会赋值为 onInterceptTouchEvent(ev)还是直接false。这里就是requestDisallowInterceptTouchEvent赋值为True将不会调用onInterceptTouchEvent的原因。
onInterceptTouchEvent如果返回true(代表要拦截此事件),那么代码将不会进入2529行的if (!canceled && !intercepted) 这个判断(透露一下,事件的分发是在这个判断里面继续的。所以不会往下分发了,后面会有分析)。而是会进入2536行的dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS)这里注意其三个参数为null;
看dispatchTransformedTouchEvent源码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
……
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
……
}
// Done.
transformedEvent.recycle();
return handled;
}
上面说了第三个参数也就是chlid为null。所以会调用super.dispatchTouchEvent。super很明显就是View,进而调用onTouchEvent 。于是就进入了问题一 中的分析。
所以结论是 很明显如果ViewGroup的onInterceptTouchEvent与requestDisallowInterceptTouchEvent会影响到ViewGroup会不会继续进行分发(就是会不会进入if (!canceled && !intercepted)中)而是直接继续往下执行最终调用父类View的dispatchTouchEvent从而最终执行到onTouchEvent。
问题三、ViewGroup如何向子View传递ACTION_DOWN事件,子View如何判断点击区域是不是自己?
继续看ViewGroup的dispatchTouchEvent,由问题二我们可知如果父布局不拦截事件,将会进入下面判断语句:
public boolean dispatchTouchEvent(MotionEvent ev) {
……
TouchTarget newTouchTarget = null;
if (!canceled && !intercepted) {
……
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
……
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 preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
……
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
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;
}
……
}
……
}
……
}
}
……
}
- 每次分发开始newTouchTarget 都会赋值为null,这个的作用后面再说。
- 如果然后得到子view的数量,如果childrenCount !=0则进入判断if (newTouchTarget == null && childrenCount != 0),这里面便是分发的逻辑。
- 首先buildTouchDispatchChildList()方法会对子view进行一个排序,(排序逻辑有兴趣可以研究下)。
- 然后循环所有的child, canViewReceivePointerEvents()方法判断child是否接受点击(就是判断下VISIBILITY或者是不是处于动画中);
- 还有一个!isTransformedTouchPointInView(x, y, child, null)判断,这个就是问题的答案之一,点进去方法里面调用了View的pointInView()方法判断点击区域是不是属于自己。
- 此时newTouchTarget 依然为空 于是来到 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))判断,此方法问题二中调用过,第三个参数为null,但这里不再是null了而是child,于是:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
……
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
- 最终调用了 child.dispatchTouchEvent,并将返回值赋给handled返回。至此事件传到子View中。
那么问题又来了,重叠的子View如和接受并禁止往下继续遍历传递呢?
- 答案跟newTouchTarget 有关 ,TouchTarget 为一个单向链表,在这里可以理解为要处理事件的子view。
- 如果子View消费此事件child.dispatchTouchEvent将返回true,最终dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign 也为true,则判断成立 将会在newTouchTarget = addTouchTarget(child, idBitsToAssign);为newTouchTarget 赋值。代表着此view就是处理事件的view,下次循环if (newTouchTarget != null)将会直接break。
问题四、为什么子View不处理ACTION_DOWN的话,将不会收到MOVE 或UP
哎呀好难解释,无非就是些判断,回头再写
总结:
每看一遍都有一遍的收获,其实就是一层一层的递归调用,网上说的什么U型传递并不恰当, 好佩服Google的工程师,期待我也能达到这个水平。