View事件分发机制

Posted 爱搬砖的摄影师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了View事件分发机制相关的知识,希望对你有一定的参考价值。

点击事件的传递规则

点击事件产生后,传递过程:Activity->Window->顶层View->分发到具体的View。前两个传递比较简单,不用说。顶层View一般为ViewGroup,ViewGroup会首先根据onInterceptTouchEvent判断是否拦截,如果拦截,那么就会调用自己的onTouchEvent方法进行处理,如果不拦截,就会分发给子View,子View再调用自己的dispatchTouchEvent做进一步分发。如果所有的View都不消费掉这个事件,那么会调用Activity的onTouchEvent方法来处理。
对于一个View而言,OnTouchListener的优先级高于自己的onTouchEvent方法,在onTouchEvent方法中,TouchDelegate的优先级最高,其次是OnLongClickListener(如果是长按的话),最后才是OnClickListener。

源码解析

当触摸事件发生时,Activity的dispatchTouchEvent首先被调用。

// Activity$dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) 
    if (ev.getAction() == MotionEvent.ACTION_DOWN) 
        onUserInteraction();
    
    // 首先分发给Window去处理,如果该事件没有被消费掉,那么
    // 会调用Activity的ouTouchEvent方法
    if (getWindow().superDispatchTouchEvent(ev)) 
        return true;
    
    return onTouchEvent(ev);
// PhoneWindow$superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) 
    return mDecor.superDispatchTouchEvent(event);
// DecorView$superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) 
    return super.dispatchTouchEvent(event);
// ViewGroup$dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) 
    ...
    // 出于安全考虑,如果设置了窗口被遮挡后丢弃事件的属性,那么窗口被
    // 遮挡时,事件直接被丢弃,而不会分发出去。也就是说,当出现了Toast、
    // 对话框等的时候,触摸事件就不会被响应了。该属性一般不会被设置。
    if (onFilterTouchEventForSecurity(ev)) 
        ...
        // 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);
            // 这个方法中会重置FLAG_DISALLOW_INTERCEPT标识,该标识表示当前
            // ViewGroup不能拦截触摸事件,也就是说,无论如何该标识都不能控制
            // ACTION_DOWN事件的拦截
            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.
            // 也就是说如果ViewGorup拦截了某触摸手势的ACTION_DOWN事件后,
            // 该触摸手势的后续事件都交给这个ViewGroup进行处理,不再需要用
            // onInterceptTouchEvent判断是否拦截
            intercepted = true;
        
        ...
    

...
// 还是ViewGroup$dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) 
    ...
    // ViewGroup不拦截时的事件分发
    for (int i = childrenCount - 1; i >= 0; i--) 
        final int childIndex = customOrder
                ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(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 (mFirstTouchTarget == null) 
        // ViewGroup进行拦截或者在子View中没有处理事件时,ViewGroup
        // 调用父类View的dispatchTouchEvent进行处理。
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    
    ...

明显,在正常的事件(通常是ACTION_DOWN)分发时,ViewGroup会依次查询每个子View,如果子View能够接收到触摸事件,就会把事件交给该子View去处理。判断是否能接收到触摸事件有两点:view没有在播放动画,也没有准备播放动画;触摸事件的坐标处于view的范围内。子View收到事件后,会调用自己的dispatchTouchEvent来进一步分发。如果子View处理了该事件,那么mFirstTouchTarget就会在addTouchTarget方法中被赋值,并且终止事件的分发。在之前的代码中,可以看到,如果mFirstTouchTarget为null,也就是没有任何子View处理该事件(通常是ACTION_DOWN),那么接下来的所有事件都会被当前的ViewGroup拦截。

// ViewGroup$dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) 
    ...
    // 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);
    
    ...
// View$dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) 
    ...
    boolean result = false;
    ...
    if (onFilterTouchEventForSecurity(event)) 
        //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在处理触摸事件时,会先判断是否设置了OnTouchListener,如果设置了而且onTouch方法处理了该事件,就不再调用onTouchEvent进行处理;否则才会调用onTouchEvent。明显,onTouch优先级高于onTouchEvent。

// View$onTouchEvent
public boolean onTouchEvent(MotionEvent event) 
    ...
    if ((viewFlags & ENABLED_MASK) == DISABLED) 
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) 
            setPressed(false);
        
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    

    if (mTouchDelegate != null) 
        if (mTouchDelegate.onTouchEvent(event)) 
            return true;
        
    

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) 
        switch (action) 
            case MotionEvent.ACTION_UP:
                // 如果在ListView等可以滚动的容器中ACTION_DOWN时,
                // 会有一个延迟之后,才会置位PFLAF_PRESSED,此前都是
                // PFLAG_PREPRESSED被置位。
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                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();
                    

                    if (prepressed) 
                        // The button is being released before we actually
                        // showed it as pressed.  Make it show the pressed
                        // state now (before scheduling the click) to ensure
                        // the user sees it.
                        setPressed(true, x, y);
                    

                    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) 
                            // 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();
                            
                        
                    
                
                break;
                ...
        
        return true;
    

    return false;

明显,只要是CLICKABLE或者LONG_CLICKABLE或者CONTEXT_CLICKABLE的,不管View是否是enabled状态,都会消费点击事件。如果View是enabled的,那么会看是否设置了代理,如果有代理,那么先调用代理的onTouchEvent方法看是否被消费掉,如果没有消费,那么才会真正由View来做处理。在ACTION_UP时如果点击事件没有被OnLongClickListener.onLongClick方法消费掉,那么就会调用OnClickListener的onClick方法。

// View$performLongClick
public boolean performLongClick() 
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

    boolean handled = false;
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnLongClickListener != null) 
        handled = li.mOnLongClickListener.onLongClick(View.this);
    
    if (!handled) 
        handled = showContextMenu();
    
    if (handled) 
        performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    
    return handled;
// View$performClick
public boolean performClick() 
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) 
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
     else 
        result = false;
    

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;

以上是关于View事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章

Android View 的事件分发原理解析

Android View 事件分发机制

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )

Android中View的事件分发机制

Android 源码解析View的touch事件分发机制

View事件分发机制