在前面的两个章节中,我们已经分析过关于Android事件处理机制的过程,特别是关于View的触摸、点击、长按之间的处理过程的分析,如果对这方面还不熟悉的读者请先阅读:
Android开发知识(七):Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(上)
Android开发知识(八):Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(中)
在本章节是我们分析Android事件处理机制的<下>篇,我们将分析关于手指从触摸屏幕到离开屏幕期间,从*ViewGroup到View的事件传递过程。
实际上,当我们手指触摸屏幕的时候,事件最先是传递给当前的Actvity,由Actvity的dispatchTouchEvent方法来分发事件,而Actvity会将事件传递给Window对象来分发,Window对象再传递给Decor View,而Decor View则是我们在Actvity中通过setContentView后所设置的布局的父容器,通过getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)这个方式就能获取到Activity所设置的布局。
我们不要单纯瞎说,还是从源码解析入手,才具有说服意义。事件从Actvity的dispatchTouchEvent开始,该方法的源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//这是一个空方法
onUserInteraction();
}
//传递给Window去分发事件
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//只有传递下去的事件返回false没有被消耗,则会调用onTouchEvent
return onTouchEvent(ev);
}
当ACTION_DOWN事件的时候,会先执行onUserInteraction();而onUserInteraction()方法是个空方法,事实上,当此activity在栈顶时,我们触屏、点击、或者按home,back,menu键等都会触发此方法,所以onUserInteraction()可以用于屏保。
接下来调用getWindow().superDispatchTouchEvent(ev)去分发事件,如果最终事件被消耗了,则直接返回true,否则Activity才会调用onTouchEvent方法自己来处理事件。
getWindow()是一个window对象。而Window类是抽象类,从Window类的注释里面说明了PhoneWindow类是Window类的唯一实现类。然而PhoneWindow存在于框架层中,在sdk中是没有源码的,因此我们看不到。想要查看源码则必须手动关联PhoneWindow,请读者自行百度。
PhoneWindow类的superDispatchTouchEvent方法源码如下:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
说明window对象是把事件传给mDecor去分发,mDecor就是上面我们说的Decor View。Decor View的superDispatchTouchEvent源码如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
其实也是调用ViewGroup类的dispatchTouchEvent方法。
到了这里,我们已经验证了事件的传递是:Activity->Window->decor View->ViewGroup
其实到了Decor View这里就相当于事件传递到了我们在Activity中设置的布局父容器了。
接下来就是一层层的分发下来,如果遇到某一子ViewGroup设置了拦截,才会让事件分发停止,而让这个ViewGroup处理,否则的话会传递到触摸位置对应的View上(如果有View的话)。
而默认的ViewGroup到View的传递过程,我们在前文也讲过了,这里再贴出以一个传递的流程图,在dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent这三个重要的方法中:
再来回顾一下前文说的:如果View的mOnTouchListener被设置的话,则onTouch会调用,如果onTouch返回了true,则onTouchEvent不会被调用,而mOnClickListener被设置了话,只有onTouchEvent被调用了后才会被调用。而如果子View在onTouchEvent中不返回true的话,则表示不消耗事件,事件会回传给上一级ViewGroup的onTouchEvent,如果全部ViewGroup的onTouchEvent都不返回true则最终到达Activity的onTouchEvent方法。
好了,现在我们重点来分析*ViewGroup到View的事件传递过程的源码。
我们从dispatchTouchEvent方法分析,该方法源码比较多,我们就不全部贴出来,由于Android系统版本更新,源码有所改变,但是原理是不变的。我这里是基于Android6.0的源码来分析的。
展开dispatchTouchEvent的源码,前面几句代码是关于辅助点击的,我们暂时可以忽略掉,直接绕过,看接下来的代码:
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//从拦截标志位取得是否允许拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//判断是否需要拦截,默认是返回false
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.
/*代码会走到这里,说明当前不是ACTION_DOWN事件或者是mFirstTouchTarget为null,说明没有一个子View要去处理ACTION_DOWN事件,导致mFirstTouchTarget还是空的,没有指向要处理事件的子View,所以接下来的其他事件,都不再继续分发下去了,而且拦截了事件让自己处理。
*/
intercepted = true;
}
从源码得知,ViewGroup会判断是否要拦截事件只会是在ACTION_DOWN的时候,或者mFirstTouchTarget!=null。mFirstTouchTarget其实要才从后面的代码才能得知其作用,在这里先说明一下他的作用:当事件被这个ViewGroup的某个子View处理时,mFirstTouchTarget会指向这个子View。也就是说如果这个ViewGroup不拦截事件的话,则事件交给了子View处理了后mFirstTouchTarget也就不为null了。
在if里面第一句代码:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
FLAG_DISALLOW_INTERCEPT是一个标记位,通过 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 可以设置它,一般是用在子View里面。如果FLAG_DISALLOW_INTERCEPT被设置了后,ViewGroup就无法拦截ACTION_DOWN以外的其他事件,这是因为ACTION_DOWN事件会重置FLAG_DISALLOW_INTERCEPT标记位,我们还是来看看源码吧,在我们分析dispatchTouchEvent的这段代码的前面还有这么几句:
// 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();
}
首先cancelAndClearTouchTargets方法会遍历清除所有的target,导致mFirstTouchTarget=null,然后在调用resetTouchState,重置触摸状态,点击进去看看resetTouchState的源码:
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
//重置标志位
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
从源码得知resetTouchState会重置掉FLAG_DISALLOW_INTERCEPT标志位,所以说,当子View调用requestDisallowInterceptTouchEvent方法是不会影响到ViewGroup对ACTION_DOWN事件的拦截处理。
分析到这里我们可以得出一个结论:当ViewGroup要拦截事件后,那么后续到来的事件会直接交给他处理,而不会再调用onInterceptTouchEvent询问是否拦截。
因为onInterceptTouchEvent方法会被调用的话有两个重要条件:要么当前是ACTION_DOWN事件要么有子View处理了事件,同时FLAG_DISALLOW_INTERCEPT标记位没有被设置。而ViewGroup要拦截事件的话,则子View就没处理这个事件,所以后面的其他事件也就满足不了这个条件而不会再走onInterceptTouchEvent方法。
另外我们还有一个结论就是:当调用requestDisallowInterceptTouchEvent方法设置了不允许拦截的标记位后,在ACTION_DOWN事件的时候会被重置掉而不起作用,也就是说requestDisallowInterceptTouchEvent方法针对的是ACTION_DOWN以外的其他事件,并且是在不拦截ACTION_DOWN事件的情况下才会起作用。
接下来我们来看一下当ViewGroup不拦截事件的时候,事件的分发,源码如下:
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL; // 此事件是否被取消了
// Update list of touch targets for pointer down, if needed.
// 是否要拆分事件,在Android 3.0之后才引入的,默认是拆分
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null; // 用来记录一个要处理事件的子View
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) { // 如果事件没被取消也不拦截的话
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) // 接下来其他手指的ACTION_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 View[] children = mChildren;
final boolean customOrder = isChildrenDrawingOrderEnabled();
// 遍历ViewGroupde所有子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ?
getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
/*第一个条件是判断事件的坐标是不是落在这个子View里面,第二个是判断子View是否在播放动画
*/
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
newTouchTarget = getTouchTarget(child);// 查找child对应的TouchTarget
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; // newTouchTarget已经有了,则跳出
}
resetCancelNextUpFlag(child);
// 将此事件交给child处理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 如果子View处理掉这个事件的话,将此child添加到touch链的头部
// 会更新 mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true; // 记录ACTION_DOWN事件已经被处理了。
break;
}
}
}
从上面的源码,结合我做的注释,基本就可以理解这个流程。其中有个重要的代码,就是GroupView遍历查找目标子View的时候是根据事件是否落在该子View的区域内已经子View是否在播放动画来决定是否要传递事件到该子View。
然后当判断出了一个目标子View来处理事件的时候,调用了dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法,
我们来看一下这个方法的部分重要源码:
if (child == null) {
//XXX省略
handled = super.dispatchTouchEvent(event);
} else {
//XXX省略
handled = child.dispatchTouchEvent(event);
}
}
从中我们得出,到这里会调用子View的dispatchTouchEvent进行下一轮的事件分发,因为我们在GroupView里面遍历查找满足条件的子View,所以在这里child不为null,调用的是child自身的dispatchTouchEvent方法,使得事件到了子View手里,再继续分发。
另外一点,当dispatchTransformedTouchEvent方法返回了true,也就是child的dispatchTouchEvent方法了true后,则会调用:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
addTouchTarget的源码:
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
可以发现根据坐标点找到了目标子View,并且赋值给了mFirstTouchTarget!=null。
到这里就完成了这一轮的事件分发,然后还没有结束,加入遍历完所有子View后,没有找到合适的子View来处理呢?
这里有两种情况导致这种结果:第一种是ViewGroup根本就没有子View,第二种是子View的确是有处理了,但是在dispatchTouchEvent返回了false(当然默认是返回true,只有重写View的这个方法后才可能返回false)
这种情况就看接下来的ViewGroup处理ACTION_DOWN的源码了:
// Dispatch to touch targets.
//如果遍历完没有找到合适的子View处理事件的话
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//注意第三个参数为null,而前面传的是child
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
当没有找到合适的子View处理事件的话,ViewGroup则只能自己处理事件了,同样会调用dispatchTransformedTouchEvent,不同的是第三个参数传的不是child了 而是null。
使得dispatchTransformedTouchEvent里面,child为null,调用了handled = super.dispatchTouchEvent(event);事实上,super.dispatchTouchEvent(event)就是View的默认处理方式了。
而如果mFirstTouchTarget!=null,则else里面的代码:
while (target != null) { // 遍历TouchTarget形成的链表
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true; // 已经处理过的子View不再让其处理事件
} else {
// 取消child标记
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 如果ViewGroup拦截了touch事件则把touch链上的child发送cancel事件,也就是参入cancelChild为true
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true; // TouchTarget链中任意一个处理了则设置handled为true
}
if (cancelChild) { // 如果是cancelChild的话,则回收此target节点
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next; // 删除一个节点
}
target.recycle(); // 回收
target = next;
continue;
}
}
predecessor = target; // 访问下一个节点
target = next;
}
当mFirstTouchTarget不为空则表示有子View要处理事件,则接下来的其他事件,因为不是ACTION_DOWN则不会走ACTION_DOWN事件的逻辑,直接到达这里,处理接下来的其他事件。
最后一个事件是ACTION_UP,就是ACTION_UP处理完之后,会调用resetTouchState();来重置状态。
到这里,我们就分析完ViewGroup的dispatchTouchEvent方法了。
可能有同学会问,ViewGroup的dispatchTouchEvent是走完了,但是好像没看到ViewGroup如果自己拦截的话则调用onTouchEvent啊。
事实上,ViewGroup并没有调用onTouchEvent,ViewGroup也没有去重写onTouchEvent。此话怎讲呢?
在上文我们分析到了当ViewGroup拦截了事件后,会调用dispatchTransformedTouchEvent方法,只是第三个传入的参数是null,如果不为空则会再继续下一轮分发了,所以要拦截事件的话,这里就会传如null,而传入null后,注意在dispatchTransformedTouchEvent方法里面走的是handled = super.dispatchTouchEvent(event);。注意super,ViewGroup直接继承自View,所以super.dispatchTouchEvent(event);直接变成是View类对事件分发的处理了。其实View类本身因为已经是child了,不是group,所以事件到达了View后,View只有决定要不要处理这个事件,而没有所谓得继续像下级分发。
由于View不需要完成分发的操作,所以dispatchTouchEvent方法显得非常简单。
接下来我们来看一下View类的dispatchTouchEvent源码:
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
//没有焦点则不处理这个事件,返回了false
//当focusable=false的话则会在这里被返回false
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
//返回值,默认为 false 代表不消费。
boolean result = false;
//判断是否是键盘输入事件,如果先是的话先传递给输入事件的onTouchEvent,并且还会继续执行后面的代码
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
//是判断窗口是否被遮挡,如果被遮挡则返回false,比如有时候两个View是会重叠的,导致其中一个被遮挡了。
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//此处我们也在上一篇文章分析过了,当设置了mOnTouchListener后,如果onTouch返回true则不会调用onTouchEvent
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
//判断手势来停止滚动
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
对比起ViewGroup,View的dispatchTouchEvent方法源码很简单吧。而且在上一篇文章中我们也提到过,当设置了mOnTouchListener后,如果onTouch返回true则不会调用onTouchEvent
ViewGroup里面并没有重写onTouchEvent,而关于View的onTouchEvent方法我们已经在上一篇文章Android开发知识(八):Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(中)分析完了,若有同学对这个方面不了解的请阅读上一篇文章。
如有疑问或者分析有误的话,烦请读者留下评论,谢谢。