Android自定义View事件分发流程详解

正文

事件传递和事件分发其实就是一个东西,叫法不一致罢了,你不用被名称所迷惑。有的人管这个叫事件传递机制,有的人则叫它事件分发机制。为了避免混淆,我这里统一称为事件分发。事件分发在自定义View开发中属于重点也是难点,多少人遇到瓶颈倒在这里了,所以完全有必要拿出来讲解一下。

事件分发流程

首先一个事件先从Activity的dispatchTouchEvent()方法开始。

public boolean dispatchTouchEvent(MotionEvent ev) {
 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
 onUserInteraction();
 }
 if (getWindow().superDispatchTouchEvent(ev)) {
 return true;
 }
 return onTouchEvent(ev);
}

先调用PhoneWindow的superDispatchTouchEvent()方法,然后PhoneWindow的superDispatchTouchEvent调用的是DecorView的superDispatchTouchEvent。

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
 return mDecor.superDispatchTouchEvent(event);
}

我们再看DecorView的源代码。

public boolean superDispatchTouchEvent(MotionEvent event) {
 return super.dispatchTouchEvent(event);
}

在DecorView的superDispatchTouchEvent()方法中调用了dispatchTouchEvent()方法。DecorView就是我们Activity真正的根布局了,它继承自FrameLayout。我们再看下DecorView的dispatchTouchEvent()方法。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
 final Window.Callback cb = mWindow.getCallback();
 return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

因为它调了super.dispatchTouchEvent()方法。我们再来看ViewGroup的dispatchTouchEvent()方法大概都写了些啥。

@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;
 // 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;
 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 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);
 }
 // Check for cancelation.
 final boolean canceled = resetCancelNextUpFlag(this)
 || actionMasked == MotionEvent.ACTION_CANCEL;
 // Update list of touch targets for pointer down, if needed.
 final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
 && !isMouseEvent;
 TouchTarget newTouchTarget = null;
 boolean alreadyDispatchedToNewTouchTarget = false;
 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;
 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);
 final int childrenCount = mChildrenCount;
 if (newTouchTarget == null && childrenCount != 0) {
 final float x =
 isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
 final float y =
 isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
 // Find a child that can receive the event.
 // Scan children from front to back.
 final ArrayList<View> preorderedList = buildTouchDispatchChildList();
 final boolean customOrder = preorderedList == null
 && isChildrenDrawingOrderEnabled();
 final View[] children = mChildren;
 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;
 }
 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);
 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;
 }
 // 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;
 }
 }
 }
 // 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 {
 // Dispatch to touch targets, excluding the new touch target if we already
 // dispatched to it. Cancel touch targets if necessary.
 TouchTarget predecessor = null;
 TouchTarget target = mFirstTouchTarget;
 while (target != null) {
 final TouchTarget next = target.next;
 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;
}

这里有个关键的变量mFirstTouchTarget,它的类型是TouchTarget。没有确定处理事件的控件之前,mFirstTouchTarget为空。那么就会调dispatchTransformedTouchEvent()方法来找消费该事件的控件层级。所以MotionEvent.ACTION_DOWN事件在没确定mFirstTouchTarget之前是一路传递下去的。

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;
}

那么如果是按下事件,或者mFirstTouchTarget已经确认,会先问onInterceptTouchEvent()方法,要不要拦截下来这个事件。 当然子控件如果调用了

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);

这个方法,你就没法拦截了。毕竟要征求子控件的意见。子控件通过调用getParent().requestDisallowInterceptTouchEvent()方法来要求得到这个事件。我们看下ViewGroup的onInterceptTouchEvent()方法。

public boolean onInterceptTouchEvent(MotionEvent ev) {
 if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
 && ev.getAction() == MotionEvent.ACTION_DOWN
 && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
 && isOnScrollbarThumb(ev.getX(), ev.getY())) {
 return true;
 }
 return false;
}

除了外接鼠标的左键被按下这种情况,默认都是不拦截。如果ViewGroup拦截了这个事件,会发生什么呢?首先会调它自己的dispatchTransformedTouchEvent()方法。

if (mFirstTouchTarget == null) {
 // No touch targets so treat this as an ordinary view.
 handled = dispatchTransformedTouchEvent(ev, canceled, null,
 TouchTarget.ALL_POINTER_IDS);
}

注意child传入的null。这样就会直接调super.dispatchTouchEvent(event)方法,也就是View的,而View的dispatchTouchEvent()方法会直接调onTouchEvent()方法。因为ViewGroup本身继承自View,那么就直接会回调ViewGroup的onTouchEvent()方法了,这样你就只能在当前拦截事件的ViewGroup的onTouchEvent()方法return true来消费事件了。

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);
 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;
}

dispatchTransformedTouchEvent()这个方法,主要用来将事件传递给子控件。如果有子控件,就分发给子控件的dispatchTouchEvent()方法,否则就调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.
 if (!isAccessibilityFocusedViewOrHost()) {
 return false;
 }
 // We have focus and got the event, then use normal event dispatch.
 event.setTargetAccessibilityFocus(false);
 }
 boolean result = false;
 if (mInputEventConsistencyVerifier != null) {
 mInputEventConsistencyVerifier.onTouchEvent(event, 0);
 }
 final int actionMasked = event.getActionMasked();
 if (actionMasked == MotionEvent.ACTION_DOWN) {
 // Defensive cleanup for new gesture
 stopNestedScroll();
 }
 if (onFilterTouchEventForSecurity(event)) {
 if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
 result = true;
 }
 //noinspection SimplifiableIfStatement
 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;
 }
 }
 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;
}

View的dispatchTouchEvent()方法第一次调用由于result为false就会进onTouchEvent()方法,可想而知,它一直找到最里面的那个View就直接调onTouchEvent()方法了,如果还没有找到,那么事件就流失掉了。如果找到了return true的onTouchEvent()方法,它自己的dispatchTouchEvent()方法也会return true赋值给handled变量。我们回到dispatchTransformedTouchEvent()方法这个位置看一下。

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;

然后回到ViewGroup的dispatchTouchEvent()方法。

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;
}

dispatchTransformedTouchEvent()返回true后,调用addTouchTarget()方法给mFirstTouchTarget赋值。那么我们再看下给mFirstTouchTarget赋值的方法。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
 final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
 target.next = mFirstTouchTarget;
 mFirstTouchTarget = target;
 return target;
 }

哪个控件消费了事件,也就是dispatchTouchEvent()方法返回了true时,就会给mFirstTouchTarget赋值,这样就确认了mFirstTouchTarget。

// 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 {
 // Dispatch to touch targets, excluding the new touch target if we already
 // dispatched to it. Cancel touch targets if necessary.
 TouchTarget predecessor = null;
 TouchTarget target = mFirstTouchTarget;
 while (target != null) {
 final TouchTarget next = target.next;
 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;
 }
}

注意看,当确定了mFirstTouchTarget后,再次调dispatchTransformedTouchEvent()方法,就传入了child了。也就是说,事件就一路直接到了mFirstTouchTarget的child指定的控件了。然后后面的MotionEvent.ACTION_MOVE和MotionEvent.ACTION_UP事件都会给到这个控件。

总结

我们开发自定义View的时候,通常是重写onTouchEvent()方法并return true来确定哪一层来消费这个事件。传递过程中,都能收到MotionEvent.ACTION_DOWN事件,而只有确认mFirstTouchTarget后的那个控件,才能收到全部的事件。

作者:dora

%s 个评论

要回复文章请先登录注册