android的事件分发在面试时算是高频问题,工作中也能用到,这里将事件分发、事件冲突,和NestedScrolling中的事件传递整理哈。
Android事件分发
方法说明
dispatchTouchEvent:事件分发,Activity, ViewGroup, View都有该方法,Activity和ViewGroup分发给子View, View分发给自己
onInterceptTouchEvent:拦截事件,只有ViewGroup有该方法,用于事件,ViewGroup想要处理某个事件时,可以随时对子View, say no!我这个我要处理
onTouchEvent:事件消费,Activity,ViewGroup,View都有该方法
对于开发者来说,第一个接收到事件的地方就在dispatchTouchEvent中,如果想全局不允许点击是,事件可以在这里直接返回,不进行下一步的分发。上段源码
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
- 没有view消费,我自己调用onTouchEvent,返回消费结果
PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
这里就熟悉了,调用了mDecor的superDispatchTouchEvent
DecorView#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView继承了FrameLayout,FrameLayout又继承了ViewGroup,FrameLayout中并没有重写dispatchTouchEvent, 所以就调用到了ViewGroup的dispatchTouchEvent
ViewGroup#dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...//此处省略数行
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;
//mFirstTouchTarget 不为空和Down事件,所以有子view消费的情况下,此处一直为真
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//是否禁止拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//父view是否拦截
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;
}
//很重要的标识,当前是否已经分配给target,不至于被down事件被消费两次
boolean alreadyDispatchedToNewTouchTarget = false;
//如果取消和拦截都不查找子view
if (!canceled && !intercepted) {
...此处省略数行
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...此处省略数行
//这里调dispatchTransformedTouchEvent, 最终调用了子View的onTouchEvent去确定该事件是否消费
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...此处省略数行
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
// 没有找到消费的子view,那去看看自己消费不,最终调用到了ViewGroup的onTouchEvent
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}else {
//mFirstTouchTarget的后续事件,move/up都会走这里去下发
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
//down事件是 alreadyDispatchedToNewTouchTarget 已经为true,所以down事件不会被消费两次
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...此处省略数行
return handled;
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...此处省略数行
//父view调用是child==null,调用super.dispatchTouchEvent
// 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);
}
return handled;
}
这里调用了子view的dispatchTouchEvent, 为了让咋们的布局文件接收到分发事件,其实是顶层ViewGroup(DecorView)调用布局文件的dispatchTouchEvent,各个ViewGroup逐层分发,直到有一个子View或者ViewGroup消费了事件。对于上层ViewGroup而言,View或者ViewGroup,对于他们都是一样处理。调用dispatchTouchEvent,ViewGroup调用dispatchTouchEvent就再次分发,然后咋们看哈View的dispatchTouchEvent
View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...此处省略数行
if (!result && onTouchEvent(event)) {
result = true;
}
...此处省略数行
return result;
}
子view调用了onTouchEvent,如果消费了就会返回true
总结
- 事件从ViewGroup逐级往下分发,直到找到消费的view或者viewgroup
- 子view一但消费了down后续的move和up都会分发给它(一个前提,未被父view拦截),即使onTouchEvent返回了flase
- 父view一但做了拦截,不管子view是否还想消费事件,都会被父view消费掉
- 如果没有子view消费,父view就会调用自己的onTouchEvent
关于第二点还要补充哈,为什么子view一但在down事件中返回了true,后续的事件都会分发给它,因为父View的mFirstTouchTarget 已经不为空,父View的父级View中的mFirstTouchTarget 也不为,一层层的下来。事件每次都会分发给down时返回true的view。这就是为什么,有时候我们明明已经移出控件外了,但是还是会接收到move和up事件。如果move和up事件返回false,事件最终就会调用activity的onTouchEvent
事件冲突处理
从上面的事件分发可知,ViewGroup拥有子view的绝对分配权,父view拦截事件,就没得子view啥事了。
在我们开发过程中可能遇到,在一个垂直滚动的scrollview中前提一个横向的列表,如果横向滚动列表,手指不会一直是一条直线,导致scrollview上下滚动,这样体验就不好。这个就是需要解决的事件冲突,解决这种冲突有两个方案。
Plan1:重写父onTouchEvent,监听当前页面的列表,如果列表是当前消费事件,onTouchEvent就不消费了
Plan2:在子View的down或者move事件中调用parent.requestDisallowInterceptTouchEvent()
为什么需要在子View的down和move中去调用?在父View的dispatchTouchEvent中,down事件是会去重置禁止拦截的标识,详细查看ViewGroup.resetTouchState()