前言
View作为Android应用与用户交互入口,除了展示视图外,还承担了处理用户操作的任务,比如用户的点击、长按、滑动事件等。处理点击事件的机制就是View的事件分发机制。
View的事件分发机制
当用户点击屏幕时,就会产生点击事件,这个事件信息被封装在一个类中,这个类就是MotionEvent。事件产生后Android系统会将事件传递到View的层级中,然后MotionEvent就会在View的层级中传递分发。
在View的分发机制中会设计到三个重要的方法,这三个方法承担了View事件机制的处理任务。它们分别是:
dispatchTouchEvent(MotionEvent ev)—对事件进行分发。
onInterceptTouchEvent(MotionEvent ev)—用来拦截事件,在dispatchTouchEvent中调用,这个方法存在于ViewGroup中。
onTouchEvent(MotionEvent ev)—用来处理事件
View点击事件的发生
当点击事件发生后,事件首先会传递到当前的Activity中,这个过程调用了Activity的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); //空方法,用于重写回调 } //调用当前Window的superDispatchTouchEvent方法 if (getWindow().superDispatchTouchEvent(ev)) { return true; } //调用Activity的onTouchEvent方法 return onTouchEvent(ev); }//PhoneWindow中的方法public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } public boolean superDispatchTouchEvent(MotionEvent event) {//调用ViewGroup中的dispatchTouchEvent方法 return super.dispatchTouchEvent(event); }复制代码
可以看到当事件产生后,首先在当前Activity中会进行事件拦截,如果当前Window不拦截就会调用Activity的onTouchEvent方法。
事件分发开始
同时,我们可以看到在PhoneWindow中会调用DecorView的superDispatchTouchEvent方法。这个方法又调用了dispatchTouchEvent方法。这是就开始了View层级的事件分发。
从上面的代码中可以看到。在View层级中,事件处理从ViewGroup的dispatchTouchEvent方法开始。我们开始从这里分析。
public boolean dispatchTouchEvent(MotionEvent ev) { //...... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // Handle an initial down. //一个完整的事件从DOWN事件开始,UP事件结束 if (actionMasked == MotionEvent.ACTION_DOWN) { // 重置触摸状态,因为程序可能由于切换、ANR或者某些其他状态改变。框架已经删除了up和cancel事件 cancelAndClearTouchTargets(ev); //重置状态 resetTouchState(); } // Check for interception. //检查是否有拦截事件 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //这个标志也跟requestDisallowInterceptTouchEvent有关,通过此函数设置标志可以另子View决定父容器是否拦截子View事件 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { //调用onInterceptTouchEvent拦截事件 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; } //...... // Update list of touch targets for pointer down, if needed. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { //不拦截事件,继续分发事件 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; //...... 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 ArrayList preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; //遍历子View for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); //获取点击范围内的字View final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; //获取touchTarget 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; } //...... } if (preorderedList != null) preorderedList.clear(); } //...... } } // 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 { 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; //父容器不拦截情况下,分发事件到对应的子View if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } //...... predecessor = target; target = next; } } //...... return handled; }复制代码
从代码中可以看到,事件分发的情况分为两种,一种是如果父容器不拦截事件,就把事件分发到对应的子View;另一种是父容器拦截事件,事件交由自己处理。在第一种情况下,ViewGroup会遍历子View,判断子View是否在点击区域内,如果是就将事件交由子View分发。第二种情况下,ViewGroup拦截事件。这个两种情况最终都会调用dispatchTransformedTouchEvent方法。接下来分析这个方法的作用。
//最终分发事件的方法private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; //......省略部分代码 // Perform any necessary transformations and dispatch. if (child == null) { //父容器拦截事件,调用View中的dispatchTouchEvent 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()); } //父容器不拦截事件,将事件分发到子View中 handled = child.dispatchTouchEvent(transformedEvent); } // Done. transformedEvent.recycle(); return handled; }复制代码
从代码中可以看到,这个实现了刚才ViewGroup中分发事件的两种情况,父容器拦截以及不拦截。拦截的情况下child就为null,这个时候调用View的dispatchTouchEvent方法。不拦截的情况下调用child的dispatchTouchEvent方法。我们再来分析下View中的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent event) { //.......此处省略部分代码 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; } } //...... return result; }复制代码
从代码中可以看到,在View的dispatchTouchEvent方法中,如果OnTouchListener不为null,就优先调用OnTouchListener的onTouch方法,并且会返回true,表示该事件被消耗。否则会调用onTouchEvent方法。在这里我们只分析onTouchEvent方法。
public boolean onTouchEvent(MotionEvent event) { //...... final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; //如果View可以点击,处理点击事件 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { //处理UP事件 case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if (!clickable) { //取消长按事件 removeLongPressCallback(); break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { //获取焦点 boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { //处理点击事件 performClick(); } } } } mIgnoreNextUpEvent = false; break; //处理DOWN事件 case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: //...... break; case MotionEvent.ACTION_MOVE: //...... break; } return true; } return false; }复制代码
从代码中可以看到,onTouchEvent处理了分发过来的事件。事件类型有ACTION_UP、ACTION_DOWN、ACTION_MOVE、ACTION_CANCEL。onTouchEvent处理事件的前提是View是可点击的。其中当View注册了OnCLickListener和onLongClickLinster即为可点击的。可以看到处理点击事件是在ACTION_UP中处理的,通过调用perfromClick方法实现,当调用点击事件时,说明长按事件未到达长按的时间。而长按事件是在ACTION_DOWN中实现的,通过checkForLongClick方法发送延迟消息,当达到长按时间时就调用长按事件。
事件分发的原理
经过上面的分析,现在总结一下View事件分发的原理。事件的开始是从Activity到PhoneWindow中,最后经由View层级。在View的层级中从顶级View(DecorView)分发。
当点击事件产生后,有顶层的ViewGroup分发事件。
通过调用dispatchTouchEvent方法,当父容器拦截事件时就调用View的dispatchTouchEvent方法,进而调用onTouchEvent方法或者OnTouchListener的onTouch方法。
否则,调用子View的dispatchTouchEvent方法。如果子View是ViewGroup类型,则继续按照步骤1分发事件。否则调用View的dispatchTouchEvent方法。
总结
View的事件分发机制,处理了用户通过触摸屏幕产生的事件。一般来说通过View的事件分发,我们经常需要处理的有DOWN、MOVE、UP事件。通过实现这些类型的事件,就可以实现不同的交互操作,进而丰富View与用户的交互体验。