前言
在 Android 的世界中无论是开发还是面试,都绕不过 事件分发 这个坎,透彻地了解事件分发一方面可以让我们在开发过程中实现炫酷的效果,同时也可以在面试中如虎添翼。
1. 什么是事件分发
所谓事件分发指的是当手指触摸手机屏幕所发生的一系列事件。一般来说通常是 ActionDown 事件开始,经过 n 个 ActionMove 事件,最后通过 ActionUp 或 ActionCancel 完成整个事件链条的消费。
当 1 个手指触摸手机屏幕时,发生上述的一系列事件的传递就是将这个事件进行分发。
1.1 事件分发
从源码中知道,当手指触摸屏幕时,事件传递是从 Activity 的 dispatchTouchEvent 方法开始,从源码中侧面看出一个事件的发生是从 ACTION_DOWN 开始的。同时可以看到把事件交给了 getWindow(),这个方法返回的是一个 window 对象,在 Android 中只存在一个window对象,也就是PhoneWindow。因此去到 PhoneWindow 中去看 superDispatchTouchEvent 方法。
#Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
//这里可以侧面
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
从源码中看到返回了 mDecor 对象的 superDispatchTouchEvent 方法,其中 mDecor 也就是我们经常说的 DecorView ,因此我们接着继续进到 DecorView 中去找 superDispatchTouchEvent 方法。
#phoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
从 DecorView 的源码中看到,DecorView 继承自 FrameLayout,而 FrameLayout 又继承自 ViewGroup,因此可以说 DecorView 继承于 ViewGroup。所以这里 super.dispatchTouchEvent(event) 这个方法调用到的其实就是 ViewGroup 的 dispatchTouchEvent 方法。因此将事件传递到 ViewGroup 的 dispatchTouchEvent 方法。
#DecorView
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
-----------------------------------
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
在 ViewGroup 中有 onInterceptTouchEvent 方法。它来代表事件是否继续往下传递。当它返回 true 时,表示拦截事件,不再往下传递,由 ViewGroup 自己消费。
当 onInterceptTouchEvent 返回 false 时,表示 ViewGroup 不拦截,继续往下传递。当传递到最后一个 View 的 dispatchTouchEvent 方法时,会传递给 onTouchEvent 进行处理,返回 true 表示 View 自己处理,而如果返回 false 表示不进行处理,就返回给上层的 ViewGroup 进行处理。
2. View & ViewGroup
前面说过了 ViewGroup 是 View 的子类,他们都有 dispatchTouchEvent 方法,那他们二者的区别是什么呢?
1) ViewGroup 中的 dispatchTouchEvent 方法先走分发流程,然后走处理流程。
2) View 中的 dispatchTouchEvent 方法负责处理事件。
-
分发流程:指的是一个事件从开始出现到被消费的过程。
-
处理流程:指的是一个事件消费的具体流程。
3. 消费流程
由于在分发的过程可能多次遇到事件消费流程,因此这里先讲解消费流程,避免重复赘述。上面说到 View 中的 dispatchTouchEvent 方法负责处理事件,因此我们进到 View 的 dispatchTouchEvent 方法。
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.isTargetAccessibilityFocus()) {
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
}
//这个 result 表示事件是否被消费
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//这个mOnTouchListener 就是平时我们set 的那个 —— 1
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果 result 为 false 才执行 onTouchEvent 方法 —— 2
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
上面注释 1 这里表示的是当存在 mListenerInfo 时,且 mListenerInfo
的 onTouch 方法返回 true 时表示事件被消费。当 onTouch 方法返回 false 时走到 注释2,执行 onTouchEvent 方法。我们先来看看这个 mListenerInfo 是什么:
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
从上面看到了对 mListenerInfo 进行初始化,那什么时候调用这个方法呢?这时候看到 li.mOnTouchListener,我们来看下这个 mOnTouchListener 什么时候初始化?看到这个 setOnTouchListener 方法,是不是很熟悉呢,是的没错,就是我们平时经常写的那个 set XXX Listener.
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
接下来进到 onTouchEvent 方法里面去看看是如何消费事件的。这里拿经常消费的 click 事件进行讲解:
#onTouchEvent
case MotionEvent.ACTION_UP:
performClickInternal();
继续进到 performClick 方法里去看看
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
走到 performClick 这个方法相信都不陌生了吧,跟上面 onTouch时一样的判断,而稍微不同的是,这里执行完 onClick 事件,直接 result = true; 表示事件被消费。因此可以说在 View 的 dispatchTouchEvent 方法里先执行 onTouch 操作,如果 onTouch 消费事件(返回 true)则由 onTouch 完成事件处理,否则(返回 false)交给 onClick 消费事件。
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
//跟 onTouch 进行一样的判断,消费事件
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
通过事件处理流程可以知道,一个事件交给 View 处理时首先交由 onTouch 处理,同时询问是否消费事件如果 onTouch 消费事件,就不会经过 onClick 事件。如果 onTouch 不消费事件再交由 onClick 处理同时消费事件。
4.分发流程
上面说了分发流程一般是发生在 ViewGroup 的 dispatchTouchEvent 方法,因此进到 ViewGroup 的 dispatchTouchEvent 方法看看是如何进行事件分发的。
#ViewGroup
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 1————> 当发生 ACTION_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();
}
// 2——————>通过这个标志位进行拦截检测
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//3————>通过 FLAG_DISALLOW_INTERCEPT 对 intercepted 进行赋值
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//4——-——>通过 onInterceptTouchEvent 方法返回值对 intercepted 赋值,判断是否拦截
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 intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 5————>事件取消标志位
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
//15——————>初始化 alreadyDispatchedToNewTouchTarget
boolean alreadyDispatchedToNewTouchTarget = false;
//6————>事件不拦截,也就是事件往下分发
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//14————>Move事件此处进不去
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 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);
//8————>获取该 ViewGroup 下所有子View 个数
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.
// 9————>通过这个方法将ViewGroup里所有的子View根据 Z 轴排列,越先绘制的View 排列越后。
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//10——————>将排列好的 view 倒序取出,
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !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);
//11————>又是这个方法,进行分发 or 处理
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();
//12 对 newTouchTarget 赋值,为下面事件处理做准备。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 7————>处理消费事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
//13————>根据注释12处的值进行遍历
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;4
// 根据12处可得知进入 if 语句,也就是消费事件
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
对上面源码进行按功能进行分块处理,首先是 注释1 处,当发生 ACTION_DOWN 事件时,会对执行一个重置状态的操作:
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
接下来是 intercepted 标志符,通过它来表示事件拦截或者事件分发。其中 注释2、3、4 是不同条件下对 intercepted 标志符 的赋值。而 注释5 则是取消事件的标志位:canceled。从这里可以侧面反映出 Android 中的事件还有 ACTION_CANCEL。
事件分类4.1 拦截事件
我们暂时先将 intercepted 设置为 true,canceled 设置为 false ,把这个当成先决条件往下看。
由于 intercepted == true,canceled == false,因此在 注释6 处,不会进到这个代码块中,同时因为 mFirstTouchTarget 此时为 null,因此进到 注释7 处,执行 dispatchTransformedTouchEvent 方法,此时传进去的 第3个 传参为 null,表示没有 子View,我们进到 dispatchTransformedTouchEvent 这个方法来看看做了什么
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
//1)————>这里进行判断是进行分发还是处理
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// 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 的判断,此时传进来的 child 是 null,因此走了 super.dispatchTouchEvent(transformedEvent)。前面说过 View 负责处理消费事件,因此这里的 super 回调到 View 中的 dispatchTouchEvent 方法,在里面通过 onTouch 或者 onClick 进行事件消费处理。
4.2 分发事件
在这种情况下将 intercepted 标志位置为 false 表示不拦截事件。因此可以进入 注释6 处代码块。在这里进行一系列的事件处理。在 注释8处获得该 ViewGroup 下的所有 子View 个数。在 注释9 处通过 buildTouchDispatchChildList 方法将所有 子View 按 Z 轴排列:
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;
//越在前面的View 排列在越靠后
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
从上面源码可以看出 越在前面的View 排列在越靠后,举个例子说明,在xml 文件中如果按这样排列的话:
xml文件 那么生成的 ArraryList 就是: 倒序排列的 ArraryList接着走到 注释11 处,又看到前面说过的熟悉的方法 dispatchTransformedTouchEvent 。注意此时不同的是,现在这里传进去的不再是 null ,而是 child。我们回到前面 dispatchTransformedTouchEvent 方法,此时走了 child !=null 的代码块,此时调用了 ** child.dispatchTouchEvent(event); ** ,看到这里不禁发出一句 卧槽,原来是 递归,没错,就是通过 递归 去 分发,消费 事件。
当走到 注释12 时,我们要注意这几个变量的值,通过 addTouchTarget 方法进行赋值:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
通过这个方法,以及 注释12 处可以得到:
- mFirstTouchTarget = taget
- target.next = null
- alreadyDispatchedToNewTouchTarget = true;
我们带着这3个变量的值继续往下走,看到前面的 注释7 处,此时 mFirstTouchTarget 已不为空,因此走了 else 语句。在 注释13 处,以及 target.next = null 这个条件,可以得出 while 循环至少走一遍,当发生多指触控的情况下时,这个 while 循环则会走多遍。最后从前面记录的 mFirstTouchTarget = taget 以及 alreadyDispatchedToNewTouchTarget = true; 得出走了 if 代码块,这里直接将 handled 置为 true 返回,告诉上层此时事件已经处理完毕。
5. ACTION_MOVE
继续看前面代码,当发生 ACTION_MOVE 事件时,注释1 处不再重置状态,然后到了 注释6 处进入事件分发,但在 注释14 处,因为是 ACTION_MOVE 事件,所以这个 if 语句进不去。因此直接走到 注释7 处(分发与处理事件)注意此时,mFirstTouchTarget != null ,alreadyDispatchedToNewTouchTarget = false(注释15 进行初始化,由于没有进入分发,所以为false)。因此到了 注释12 时,走了 else 语句,此时 cancelChild 为 false,又到了熟悉的 dispatchTransformedTouchEvent 方法,继续去分发事件。如果为 ViewGroup 则继续递归分发,如果为 View 则结束分发,消费事件。
网友评论