浅析Android View事件分发机制(应用侧)

基础概念

  1. 触摸事件:手指触摸屏幕时生成的事件,即MotionEvent。常见的触摸事件有:ACTION_DOWNACTION_MOVEACTION_UP以及ACTION_CANCEL,当存在多个手指触摸屏幕时,还会触发ACTION_POINTER_DOWNACTION_POINTER_UP事件。
  2. 焦点事件:ACTION_DOWNACTION_POINTER_DOWN属于焦点事件,通过MotionEvent中的PointerId进行描述;
  3. 触摸事件序列:从手指触摸屏幕开始到手指离开屏幕结束的过程中触发的一系列事件,通常以ACTION_DOWN事件开始、ACTION_UP事件结束,中间有不定数量的ACTION_MOVEACTION_POINTER_DOWN或者ACTION_POINTER_UP事件的一系列事件。
  4. 滑动冲突:View树中相邻的层级上均存在可滑动的View,当用户触摸屏幕时触发了ACTION_MOVE事件导致有多个View可以处理的情况。
  5. 事件分发机制:触摸屏幕产生的事件MotionEvent在整个View树上分发处理的逻辑,理解事件分发机制的实现原理才能知道如何解决View滑动冲突问题。

源码分析

事件分发流程

当用户开始触摸手机屏幕时,经过传感器等一系列硬件处理,最终生成触摸事件并由SystemServer进程的InputMangerService服务通过Socket发送到目标应用进程,经过多个的InputStage分发之后触摸事件被传递到DecorView#dispatchPointerEvent方法,dispatchPointerEvent方法内部调用了dispatchTouchEvent方法,通过PhoneWindow#getCallback方法拿到目标Activity,最终调用Activity#dispatchTouchEvent方法进行处理。

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }
}

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
   		// mWindow是PhoneWindow,cb是Activity
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
}
    
public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback,
        ContentCaptureManager.ContentCaptureClient {
	/**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     *
     * @see #onTouchEvent(MotionEvent)
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 通过Window进行分发最终交由根布局DecorView进行处理,如果处理成功则将事件从队列中移除,否则交由onTouchEvent继续处理;
        if (getWindow().superDispatchTouchEvent(ev)) { 
            return true;
        }
        // 如果整个View树都没有处理触摸事件,则由Activity的onTouchEvent处理
        return onTouchEvent(ev); 
    }

	/**
     * Retrieve the current {@link android.view.Window} for the activity.
     * This can be used to directly access parts of the Window API that
     * are not available through Activity/Screen.
     *
     * @return Window The current window, or null if the activity is not
     *         visual.
     */
    public Window getWindow() {
        return mWindow;
    }

	final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
            IBinder shareableActivityToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
        mActivityInfo = info;

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        
		// 省略其他代码
	}
}

接着,触摸事件会被分发给目标ActivityPhoneWindow(在Activity实例创建之后调用attach方法中进行创建)进行处理。经过PhoneWindow#superDispatchTouchEvent方法将触摸事件交由DecorView.superDispatchTouchEvent。因为DecorView继承自FrameLayout,而FrameLayout继承自ViewGroup,所以触摸事件最终由ViewGroup.dispatchTouchEvent开始分发处理。

public class PhoneWindow extends Window implements MenuBuilder.Callback {
	@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

	private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        // 省略其他代码
    }
 }
 
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
	public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
}

至此,触摸事件已经被分发到View树的根节点,开始在View树上进行遍历(深度遍历)和分发处理,总结流程图如下:
在这里插入图片描述

为了侧重分析触摸事件的分发流程,下面主要围绕关键代码进行分析,和事件分发流程关联不大的部分直接略过;

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

	// First touch target in the linked list of touch targets.
	// 记录当前父View下第一个处理触摸事件的View对象,内部通过链表的形式进行维护
    @UnsupportedAppUsage
    private TouchTarget mFirstTouchTarget;
    
	@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        // 过滤不安全的触摸事件
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // STEP 1:ACTION_DOWN事件标识一个事件序列的开始,需要重置之前事件处理过程中的标记以及中间状态;
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev); // 清空之前事件序列处理过程中的所有TouchTarget
                resetTouchState(); // 重置之前事件序列处理过程中的触摸状态标记
            }

            // 检查是否需要拦截
            final boolean intercepted;
            // STEP 2:根据事件类型以及是否存在前序事件被子View消费来判断是否调用onInterceptTouchEvent方法让当前ViewGroup尝试拦截,如果父容器不拦截则直接分发给子View处理(如果没有子View能处理触摸事件则交由当前ViewGroup处理),否则直接由当前ViewGroup进行处理;
            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 {
                // 前序事件一直都是当前ViewGroup处理,所以当前ViewGroup继续处理本次事件
                intercepted = true;
            }
            
            // 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) {
                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) {
                    	// 获取当前触摸事件在当前ViewGroup中的相对坐标 x, y 值
                        final float x = ev.getXDispatchLocation(actionIndex);
                        final float y = ev.getYDispatchLocation(actionIndex);
                        // Find a child that can receive the event.
                        // 从前到后遍历子View来寻找能够接收本次事件的子View
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        // STEP 3:遍历并分发触摸事件给所有的子View进行处理
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                            ...

                            // 判断触摸事件是否可以被子View响应,canReceivePointerEvents方法根据View可见性和动画进行判断,isTransformedTouchPointInView判断触摸事件是否落在View的区域内(兼容了属性动画)
                            if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            // 判断子View是否消费过之前的触摸事件,如果消费过之前的触摸事件,则直接结束遍历,进入下面的处理流程,会将事件再次分发给这个子View
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            // STEP 4:将触摸事件交给子View进行处理
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                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 = x;
                                mLastTouchDownY = y;
                                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之后触摸事件没有被消费,则交给之前最早消费触摸事件的View进行处理
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // STEP 5:如果没有子View能够处理触摸事件,则交给当前ViewGroup进行处理
            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;
                    // 之前遍历子View的过程中已经消费过触摸事件的子View
                    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.
            // 如果取消或者ACTION_UP事件发生则清空所有触摸状态
            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;
    }


	/**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; // 清除子View对父View的拦截标志
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
}

DecorView#superDispatchTouchEvent方法梳理出来的主要流程:
首先,ViewGroup#dispatchTouchEvent会先判断是否需要由ViewGroup拦截事件,如果触摸事件没有被DecorView拦截的情况下,则会将触摸事件分发给所有的子View,根据子View的类型进行进一步的分发:如果子ViewViewGroup那么会重复上面的过程,否则会通过View#dispatchTouchEvent方法来处理触摸事件,如果子View不消费触摸事件,则分发给下一个子View进行处理,直到触摸事件被某个View处理或者整个View树都没有处理,整个事件分发流程就结束了。

下面针对这个流程中的主要步骤进行分析:

STEP 1:首先,如果当前触摸事件是ACTION_DOWN事件,那么会重置ViewGroup的标识位,所以子View无法禁止其所在的ViewGroupACTION_DOWN事件的拦截动作的,因此ACTION_DOWN事件一定会先被ViewGroup#onInterceptTouchEvent进行处理;

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

STEP 2:根据触摸事件的类型以及前序触摸事件是否有对应的处理者来决定是否由当前ViewGroup进行拦截:

  • 如果当前触摸事件是ACTION_DOWN,则disallowIntercept变量的值一定为false,因此直接调用ViewGroup#onInterceptTouchEvent方法判断是否拦截触摸事件;
  • 如果当前触摸事件不是ACTION_DOWN,但是本次事件序列中的前置事件已经被某个子View消费,即mFirstTouchTarget不为null,那么判断是否存在子View调用ViewGroup#requestDisallowInterceptTouchEvent禁止当前ViewGroup拦截触摸事件,禁止的情况下当前ViewGroup无法拦截,否则调用当前ViewGroup#onInterceptTouchEvent方法判断是否拦截;
  • 如果当前触摸事件不是ACTION_DOWN,并且之前的触摸事件没有被当前ViewGroup的任意一个子View消费过,则当前触摸事件直接被当前ViewGroup进行拦截,因此如果子View没有消费ACTION_DOWN,那么同一个事件序列中的后续触摸事件默认交由当前ViewGroup进行处理;
	// 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;
    }

总结如下:

  • 对于ACTION_DOWN类型的触摸事件,一定会调用ViewGroup#onInterceptTouchEvent方法尝试拦截;
  • 对于非ACTION_DOWN类型的触摸事件,如果前序事件没有被子View消费过,一定会被ViewGroup拦截,即不会分发给子View
  • 对于非ACTION_DOWN类型的触摸事件,如果前序事件有被子View消费过,那么会判断是否存在子View禁止ViewGroup拦截事件,如果没有就会调用ViewGroup#onInterceptTouchEvent方法尝试拦截,否则不会拦截,直接分发给子View进行处理;

STEP 3&4:根据当前ViewGroup是否拦截触摸事件(变量intercepted的取值)来决定是否遍历子View来处理本次触摸事件,如果当前ViewGroup拦截触摸事件则不会遍历子View来分发处理触摸事件,直接判断mFirstTouchTarget是否为null,如果mFirstTouchTargetnull,说明目前没有子View处理过本次触摸事件序列中的触摸事件,则直接交由当前ViewGroup调用View#dispatchTouchEvent进行处理。否则从mFirstTouchTarget开始遍历之前处理过触摸事件的子View处理触摸事件;

否则,先遍历子View对本次触摸事件进行处理,如果触摸事件落在了某个子View的区域内,那么就调用ViewGroup#getTouchTarget来查找当前子View是否在之前处理过触摸事件,如果之前已经处理过触摸事件,则更新其处理过的pointerIdBits,否则调用ViewGroup.dispatchTransformedTouchEvent方法来间接调用View#dispatchTouchEvent处理本次触摸事件。

STEP 5:如果遍历所有子View之后此触摸事件没有被处理,则交由当前ViewGroup进行处理,最终调用到当前ViewGroupView#dispatchTouchEvent方法来处理本次触摸事件;

整理流程如下图所示:
在这里插入图片描述

注意点:ViewGroup负责拦截和分发触摸事件,View负责处理触摸事件;

事件处理流程

触摸事件经过ViewGroup分发之后,非容器类的View就可以对落在自身区域中的触摸事件进行具体的处理,如果最终成功消费了触摸事件,那么这次触摸事件的分发处理流程就结束了。

接着看下非容器类的View是如何处理触摸事件的。

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    /**
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        // ...
        boolean result = false;
		// ...
		
		// 1. 先对触摸事件进行安全性校验,如果通过了才会进入后续的处理流程;
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            // 2. 如果View设置了OnTouchListener对象,则优先将触摸事件交给其进行处理;
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

			// 3. 如果没有设置OnTouchListener或者OnTouchListener的onTouch调用后返回false,那么就调用onTouchEvent方法进行处理。
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
		
		// ...
		
        return result;
    }
}

从源码可以看出View.dispatchTouchEvent进行了以下处理:

  1. 如果子View设置了OnTouchListener则调用其onTouch方法进行处理;
  2. 如果没有设置OnTouchListeneronTouch方法返回false,则交由View.onTouchEvent进行处理;

由此可见,触摸事件会先被交由OnTouchListener进行处理,如果没有被OnTouchListener处理,才会走到onTouchEvent方法。下面看下onTouchEvent方法的内部实现。

    /**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        // 是否可点击
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        // 是否enable
        if ((viewFlags & ENABLED_MASK) == DISABLED && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false); // 取消按压态
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // 注意:即使View是disabled的,只要其是可点击的,那么触摸事件就会被直接消费,只是没有做任何响应。
            return clickable;
        }
        // 如果有代理则交由代理处理
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
		// 如果可点击或者设置了TOOLTIP标记,则尝试进行处理,否则直接不处理
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    // ACTION_UP:如果View变为不可点击则移除callback并结束处理流程
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // ...
                        
                        // ACTION_UP:如果长按动作任务没有执行,则移除长按回调并执行点击处理
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();
                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // 通过抛任务来保证先展示可见状态,然后执行点击动作触发的任务
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        // 重置点击相关的状态
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    // ...
                    mHasPerformedLongPress = false;
                    // ACTION_DOWN:如果View不可点击则通过post一个延迟任务(默认400ms)来检测是否为长按行为
                    if (!clickable) {
                        checkForLongClick(ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    // ACTION_DOWN:如果所在的容器是可滑动的,则延迟处理本次事件(默认100ms)
                    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
                        // ACTION_DOWN:如果所在的容器是不可滑动的,则设置按压态并post一个延迟任务(默认400ms)来检测是否为长按行为
                        setPressed(true, x, y);
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    // ACTION_CANCEL:如果View可点击,则清除按压态
                    if (clickable) {
                        setPressed(false);
                    }
                    // ACTION_CANCEL:移除所有callback
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;
                                        
                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    // ACTION_MOVE:如果移动到了View范围以外,则移除所有callback
                    if (!pointInView(x, y, touchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }

                    final boolean deepPress = motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                    if (deepPress && hasPendingLongPressCallback()) {
                        // process the long click action immediately
                        removeLongPressCallback();
                        checkForLongClick(0 /* send immediately */, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                    }

                    break;
            }

            return true;
        }

        return false;
    }

通过上述源码分析,梳理整个触摸事件序列的处理流程如下:

  1. 如果View是可点击的但是被disabled了,则直接返回true,否则继续执行;
  2. 如果设置了TouchDelegate则调用其onTouchEvent方法进行处理,如果onTouchEvent方法返回true,则直接返回true
  3. 否则,判断View是不是可点击的或者设置了TOOLTIP标记,如果不是可点击的且没有设置TOOLTIP标记,直接返回false;否则根据事件类型进行对应的处理,但是最终返回的值为true
  4. 根据触摸事件类型进行不同的处理:

ACTION_DOWN事件:如果View不可点击则通过post一个延迟任务(默认400ms)来检测是否为长按行为,否则,判断所在的容器是不是正在滑动,是的话则延迟处理本次事件(默认100ms),否则设置按压状态并post一个延迟任务(默认400ms)来检测是否为长按行为;

ACTION_UP事件:如果View不可点击则移除所有callback并结束处理流程,否则移除长按检测callback,并按照点击行为来执行相关的点击任务;
ACTION_MOVE事件:如果触摸事件的坐标已经离开了View的范围,那么移除所有callback,即不会执行View的任何逻辑,因为已经不在响应区域了;
ACTION_CANCEL事件:移除所有callback
由此可见,View的点击行为是通过ACTION_DOWNACTION_UP事件都被同一个View消费来触发执行的,View的长按行为是通过在ACTION_DOWN事件发生之后向消息队列抛延迟任务来实现的。

思路借鉴

Android事件分发机制的源码实现中存在一些比较不错的思路,可以在日常开发借鉴学习。

一、模版方法模式:在分发触摸事件给子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;
    }

二、职责分离:ViewGroup负责拦截和分发触摸事件,View负责处理触摸事件;

	/**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            	final boolean handled;
            	
            	// 省略其余代码
            	
				// 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;
            }

三、缓存加速:TouchTarget记录上次触摸事件的处理者,加速后续触摸事件的分发处理;

  1. 遍历子View分发触摸事件,记录处理触摸事件的子ViewTouchTarget
	/**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
  1. 新的事件序列开始或者发生取消事件时,清空之前的TouchTarget记录;
	/**
     * Clears all touch targets.
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }

四、动画处理
通过对触摸事件进行转换处理,同时兼容子View的动画,保证点击事件在动画区域得到响应;

	if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值