我们用手指点击手机屏幕之后,经过底层传递之后传给了Activity,首先由Activity的dispatchTouchEvent方法接收,整体的流程如下:
-->Activity.dispatchTouchEvent(MotionEvent ev)
--->PhoneWindow.superDispatchTouchEvent(MotionEvent ev)
---->DecorView.superDispatchTouchEvent(MotionEvent ev)
---->ViewGroup.dispatchTouchEvent(MotionEvent ev)
---->View.dispatchTouchEvent(MotionEvent ev)
----->View.onTouchEvent(MotinEvent ev)
来通过下面一张霸气的图来帮助我们理解
DecorView本质上就是一个FrameLayout,而FrameLayout并没有重写
dispatchTouchEvent,所以直接进入了ViewGroup的dispatchTouchEvent
Android的事件分发主要关注以下几个核心方法:
1.dispatchTouchEvent: 负责事件的分发,该方法在Activity、ViewGroup、View中都有
2.onInterceptTouchEvent:负责事件拦截,只有在ViewGroup中有此方法
3.onTouchEvent: 负责事件消费,在Activity、View中有此方法
Android触摸事件的类型:手指触摸到手机屏幕的一瞬间会触发 一个 ACTION_DOWN 事件,之后手指开始在屏幕上移动会触发 **多个ACTION_MOVE事件,**最后手指抬起的瞬间会触发 一个ACTION_UP事件
首先看一下ViewGroup的dispatchTouchEvent方法
ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {//触摸事件安全性检查,正常返回true
final int action = ev.getAction();//获取事件ACTION
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
//每一次DOWN事件一定会先走到这里,以下方法之后会详细介绍
//如果之前DOWN事件被处理,接下来MOVE事件来时便 不会走这里,也就是MOVE事件状态不会被重置
cancelAndClearTouchTargets(ev);//清除TouchTarget
resetTouchState();//重置触摸状态
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//每次ACTION_DOWN一定询问是否拦截
//mGroupFlags 开始未初始化,这里disallowIntercept返回false
//如果之前DOWN事件被处理,接下来当第一个MOVE事件来时也会走这里
//因为DOWN事件被处理后 mFirstTouchTarget!=null
//子View可以通过调用getParent().requestDisallowInterceptTouchEvent()方法
//来改变 disallowInterept 变量的值,从而决定父View是否拦截子View(DOWN事件除外)
//如果是DOWN事件,由于前面调用了 resetTouchState(),所以 disallowIntercept一定为false
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 {
...
//既不是ACTION_DOWN事件,mFirstTouchTarget又为null(也就是下面讲到的cancelChild为true的情况)
//此时将会直接拦截而不再调用onInterceptTouchEvent进行询问
intercepted = true;
}
......
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
...
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//正常点击canceled=false,如果ViewGroup没有拦截会进入下面的流程
if (!canceled && !intercepted) {
......
//如果之前DOWN事件被处理,接下来当第一个MOVE事件来时将不会进入下面的逻辑
//而是直接跳过进入之后的 mFirstTouchTarget 的判断
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 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.
//buildTouchDispatchChildList()是对该ViewGroup中的子View进行排序
//排序方法是按子View的层叠顺序进行的(也就是根据所有子View的 Z坐标 的值排序)
//preorderedList数组中 Z坐标值从低到高 排列,但是后面获取的时候是从下标为size-1处获取的
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
//按层叠顺序获取子View(获取到的子View有可能又是另一个ViewGroup)
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
......
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
......
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//dispatchTransformedTouchEvent会继续向子View传递消息
//child是上边获取到的子View,false 表示cancle为false,不取消这次点击事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//child处理了事件之后返回true,接着进入下面的流程
//如果child没有处理事件将会重新进入上边的for循环继续按层叠顺序获取子View
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
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方法
//最后会将处理事件的View保存在newTouchTarget中
//同样也将该target赋值给了mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;//表示事件已经被分发
break;//事件得到处理后,这里break掉按层叠顺序获取子View的循环
}
...
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
...
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//如果DOWN事件被拦截会直接进入这里
//如果是上面提到的cancelChild为true的情况也会直接进入这里,
//并且在之后的MOVE事件里 下面的canceled参数 为false
//因为在resetCancelNextUpFlag()方法里cancel默认设置为false
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);//注意:child参数为null!!
} else {
//DOWN事件中如果事件被某View处理,mFirstTouchTarget会被赋值给该View走下面的步骤
//如果之前DOWN事件被处理,接下来MOVE事件来时将会直接进入这里
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;//这里next为null
//注意:如果之前DOWN事件被处理,接下来第一个MOVE事件来时 alreadyDispatchedToNewTouchTarget为false
//因为 alreadyDispatchedToNewTouchTarget 赋值为true是在上边的 if 语句里面
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;//DOWN事件被处理
} else {
//如果之前DOWN事件被处理,接下来当第一个MOVE事件来时将会进入这里
//此时下面的 cancleChild 仍将返回 false,接下来会调用 dispatchTransformedTouchEvent
//但是如果我们调用了 requestDisallowInterceptTouchEvent(false),之后又在
//onInterceptTouchEvent中返回true, 此时 intercepted将为true,cancleChild将为true
//特别注意:上面刚刚提到的就是 cancelChild为true的情况!
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
//特别注意下面的情况!
//cancelChild为true时 mFirstTouchTarget将会被赋值为null
//那么之后的一系列MOVE事件将不再询问直接拦截(intercept直接设置为true)
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
......
}
......
return handled;
}
进入 dispatchTransformedTouchEvent 方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
//DOWN事件时cancle为false
//如果之前DOWN事件被处理,接下来MOVE事件来时cancle仍为false, child 不变
//如果是上面提到的cancelChild为true的情况将会直接进入下面的if语句
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
//cancelChild为true的情况直接设置ACTION
//说明事件被上层拦截时将会触发 ACTION_CANCEL
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//cancelChild为true的情况执行到这里,之后在child的 dispatchTouchEvent 中
//检测到ACTION的值为ACTION_CANCEL,将会直接执行cancel的操作
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
......
// Perform any necessary transformations and dispatch.
if (child == null) {//DOWN事件被拦截后直接进入View的dispatchTouchEvent
handled = super.dispatchTouchEvent(transformedEvent);
} else {
......
//DOWN事件时最终调用了View的dispatchTouchEvent,进入了事件分发(一)中的流程
//但是也可能获取到的是ViewGroup,此时又会进入ViewGroup的dispatchTouchEvent
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;//如果child处理了事件返回true
}
当事件被某一View处理时,进入 addTouchTarget 方法,传入的child 参数就是处理事件的View
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);//进入obtain方法
target.next = mFirstTouchTarget;//next被赋值为null
//注意:mFirstTouchTarget在这里被赋值,保存了处理事件的View
mFirstTouchTarget = target;
return target;
}
进入 TouchTarget的obtain方法
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
......
//注意:这里将处理事件的View保存在了Target中并最终返回给上边的newTouchTarget变量
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
拦截处理
对于拦截的处理需要注意ViewGroup的以下代码
if (actionMasked == MotionEvent.ACTION_DOWN) {
//每一次DOWN事件一定会先走到这里,以下方法之后会详细介绍
cancelAndClearTouchTargets(ev);//清楚TouchTarget
resetTouchState();//重置触摸状态
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN //每次ACTION_DOWN一定询问是否拦截,也就是
|| mFirstTouchTarget != null) { //DOWN事件一定会调用onInterceptTouchEvent
//mGroupFlags 开始未初始化,这里disallowIntercept返回false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//Down事件一定会执行拦截方法
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
...
intercepted = true;
}
如果是ACTION_DOWN以外事件的拦截处理,可以通过调用ViewGroup的以下方法
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
...
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
...
}
如果传入的参数为 true ,将不会进入 onInterceptTouchEvent 方法, intercept 将为 false 也就是不拦截,如果传入参数为 false 将会进入 onInterceptTouchEvent 方法询问是否拦截。
滑动冲突处理
现在有一个这样的场景,一个ViewPager里面嵌套了一个ListView,这个时候就会引起滑动冲突,假设我们采用内部拦截法在ListView中重写dispatchTouchEvent进行如下处理:
public boolean dispatchTouchEvent(MotionEvent ev) {
swich(ev.getAction){
case MotinoEvent.ACTOIN_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
......
}
return onTouchEvent(ev);
}
但是运行后发现ListView的DOWN事件并没有被拦截,原因就处在以下方法中
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
//每次DOWN事件来时ViewGroup会重置不允许拦截的标志位,这样 disallowIncept就为false
//之后便会进入onInterceptTouchEvent 询问是否拦截,也就是只要是DOWN事件一定会询问是否拦截
//onInterceptTouchEvent 默认返回 false,默认不拦截,如果拦截需要重写该方法
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
最后来一张时序图(字小可以放大下页面)
注:该时序图以一个Button的点击事件为例,时序图展示了ACTION_DOWN事件从Activity传递到View的流程,View中展示的是ACTION_UP事件在View中的处理流程
后续更新