-
序言
- MotionEvent的分发机制
- 流程图
- dispatchTouchEvent()
- onInterceptTouchEvent()
- onTouchEvent()
序言
View的分发机制是比较复杂的一块机制,在日常开发中也遇到很多与view分发机制有关的问题.所以抽空总结下view的分发机制.
MotionEvent的分发机制
用户的触摸和点击事件对应的对象类型就是MotionEvent,view的事件分发过程就是对MotionEvent的分发和消费过程,而在这个过程中主要涉及到了三个方法,分别是 dispatchTouchEvent().onInterceptTouchEvent(),onTouchEvent().
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;
}
......
dispatchTouchEvent()是整个view分发流程的入口.MotionEvent在dispatchTouchEvent()中的流程为如下:
- 首先会由Activity的 dispatchTouchEvent() 方法来处理.这个方法比较简短也好理解,会调用Activity所属的Window对象的dispatchTouchEvent()方法.
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
而在PhoneWindow这个Window的具体实现类中,调用了mDecor的dispatchTouchEvent(event)方法,mDecor是Activity.setContentView()设置的顶层view,也就是从这个地方开始motionEvent开始了在view树中的分发过程.
public boolean superDispatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(event);
}
ViewGroup的dispatchToucheEvent()过程可以分为两部分:
- 判断是否拦截motionEvent以及调用oninterceptTouchEvent()处理事件
- 决定究竟哪个子view来接收没有被父view拦截的MotionEvent()事件.
在拦截MotionEvent的时候,这里有一个逻辑需要注意:如果子view声明了disallowIntercept标志位(顾名思义就是不允许父view拦截motionEvent),父view将不会再调用interceptTouchEvent()处理ACTION_UP和ACTION_MOVE,而是直接交给子view处理。但如果是ACTION_DOWN则不会这样处理,因为当ACTION_DOWN事件到来时这个标志位总是会被重置。还有一种情况就是如果mFirstTouchTarget不为空的时候,而mFirstTouchTarget代表的就是处理上一个触摸事件的子view.
// 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;
}
在处理完intercept标志位之后,接着会遍历子view来寻找处理触摸事件的view.
这里会将子view整理为一个队列,而给子view排序的依据是子view加入到父view中的顺序,即先加入的子view会排在队列前边.然后会遍历这个子view集合来找出符合分发条件的子view:这个条件就是view可见或view.getAnimation()不为空并且触摸事件的落点正好在子view中.如果找到了这样的一个子view,那么触摸事件就会交给它处理,如果没有找到则会将触摸事件交给上一个子view,如果没有上一个处理触摸事件的子view,那么触摸事件则会交给父view来处理.
{
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 ArrayList 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 - 1;
}
if (!canViewReceivePointerEvents(child)
|| !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();
}
View.onDispatchTouchEvent()
在经历了父View的dispatchTouchEvent()后,会进入到选定的子View的(这里一般为View)的onDispatchTouchEvent()中.
- 这里首先会调用view的onTouchListener,如果onTouch()返回true,那么onTouchEvent()方法则不会被调用.
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENAB*LED && 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;
}
}
至此,onDispatchTouchEvent()的流程就结束了.
总结几个结论:
- MotionEvent是必定会经过onDispatchTouchEvent()方法的,但不保证会经过onInterceptTouchEvent()和onTouchEvent();
- Activity是MotionEvent的分发的起点和终点,所有的事件会首先交给Activity处理,如果最终没有view处理的话,事件会重新交还给Activity来处理.
- 调用requestDisAllowIntercept()方法可以禁止父view拦截点击事件,但除了ACTION_DOWN事件.
onInterceptTouchEvent()
/**
* Implement this method to intercept all touch screen motion events. This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way. Events will be
* received in the following order:
*
*
* - You will receive the down event here.
*
- The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it). Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
*
- For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
*
- If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
*
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
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的onInterceptTouchEvent()默认返回false,也就是默认不拦截任何点击事件,通过javadoc注释可以知道一旦拦截后,当前的view对象会收到ACTION_CANCEL事件,接着之后所有的MotionEvent都会被交给当前view的onTouchEvent()处理,而不会再经过interceptTouchEvent().
“安卓开发艺术探索”中是这样形容这个过程的,我觉得非常形象:View体系中的事件分发就好像领导把一个开发任务向下指派给开发人员的过程一样.
- 如果这个任务的开始部分被指派给了某个开发人员,那么后续的开发任务都会被指派给他
- 相反如果这个任务如果底层的开发人员无法胜任,那么自然而然地它就会被向上传递给上一层的其它同事,这样层层上传直到回到开始分发任务的领导(没人能解决就只能领导自己背锅了).
onTouchEvent()
onTouchEvent()回调方法会具体地处理点击事件,代码如下
在enable状态下,仍然会处理和消费事件,只是不会再执行onTouchEvent()方法后面的逻辑,onTouchListener中的回调也无法得到执行.
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
下面的这段逻辑用来处理开发者设置的onClickListener中的点击事件.主要有两点逻辑:
- 只有处于pressed状态的才能执行这段代码
- 只有当view同时满足 isFocusable(),isFocusableInTouchMode(),且isFocused()为false,才会去执行获取焦点的代码;如果没有获取到焦点,则不会调用performClick()执行onClick()事件.
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
. . .
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
以上.