# View事件分发(二)

View事件分发(二)

事件分发的源码解析

Activity对点击事件的分发过程

点击事件用MotionEvent表示,当一个点击操作发生时,事件最先传递给当前Activity,由ActivitydispatchTouchEvent进行事件分发,而具体的工作是由Activity内部的Window来完成的。Window会将事件传递荷藕DecorViewDecorView一般就是当前界面的一层容器(即setContentView所设置的View的父容器),通过Activity.getDecorView()获取,先从ActivitydispatchTouchEvent开始分析。

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

分析以上代码,首先代码将交给Activity所附属的Window进行分发,如果返回true,那么整个事件循环就结束了。额如果返回了false,就代表事件没人处理,即所有View的onTouchEvent都返回了false,那么ActivityonTouchEvent就会被调用。
  接下来看Window是如何将事件传递给ViewGroup的,在Android源码中,Window是一个抽象类,而WindowsuperDispatchTouchEvent方法也是个抽象方法。
  Android源码中,Window有且仅有一个实现类PhoneWindow,以下是PhoneWindowsuperDispatchTouchEvent的实现。

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

由源码可知,PhoneWindow将事件直接传递给了DecorView

DecorView

通过((ViewGroup) (getWindow().getDecorView().findViewById(android.R.id.content))).getChildAt(0)可以获取Activity所设置的View,而上面PhoneWindowmDector就是Activity.getWindow().getDecorView()所返回的View是它的一个子View。
  目前事件传递到了DecorView这里,由于DecorView继承FrameLayout且是父View,所以最终事件都会传递给View,即事件肯定会传递到View。

DecorView对事件的分发过程

点击事件到达DecorView后,会调用ViewGroupdispatchTouchEvent方法。接下来:如果DecorViewonInterceptTouchEvent返回true,则事件由ViewGroup处理,这时,如果ViewGroupmOnTouchListenr被设置,则onTouch会被调用,否则onTouchEvent会被设置。假如两者都提供的话,onTouch会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用。
  如果顶级ViewGroup不拦截事件,那么事件会传递给它所在的点击事件链上的子View,这时,子View的dispatchTouchEvent会被调用。
  上述过程中,点击事件已经从顶级View传递到下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。

ViewGroup对事件的分发过程

首先要看的是ViewGroup对点击事件的分发过程,主要事件在ViewGroupdispatchTouchEvent方法中,由于这个方法比较长,所以需要分段说明,以下是"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在以下两种情况下会判断是否要拦截当前事件。

  1. 事件类型为ACTION_DOWN
  2. mFirstTouchTarget != null

事件类型为ACTION_DOWN时,肯定会进行拦截。而mFirstTouchTarget != null的原因是,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,即当ViewGroup不拦截事件并将事件交由给子元素处理时,mFirstTouchTarget != null成立。而反过来,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget != null不成立,并且当ACTION_MOVEACTION_DOWN事件到来时,由于actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null为false,将导致ViewGrouponInterceptTouchEvent不会再被调用,并且同一事件的其他序列将都默认交给它处理。
  上面代码中有一种额外的情况,注意FLAG_DISALLOW_INTERCEPT这个标识位,这个标识位是通过之前所说子View的requestDisallowInterceptTouchEvent方法设置的。而FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的点击事件,而这里ACTION_DOWN可以拦截的原因是因为ViewGroup在分发事件时,处理ACTION_DOWN的时候会重置FLAG_DISALLOW_INTERCEPT这个标识位,将会导致子View设置的这个标识位无效。因此,ACTION_DOWN事件总会让ViewGroup调用onInterceptTouchEvent方法来判断是否要拦截事件。以下是ViewGroup重置状态的源码。

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

这段源码的位置在之前源码的上面。这端逻辑在处理ACTION_DOWN事件的时候将FLAG_DISALLOW_INTERCEPT重置,所以子View的requestDisallowInterceptTouchEvent方法并不能影响ViewGroupACTION_DOWN的处理。

结论

从以上的代码分析出,当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onIntercept方法。其中FLAG_DISALLOW_INTERCEPT这个标识的作用是让ViewGroup不再拦截事件,当前前提是ViewGroup不拦截ACTION_DOWN事件。
  以上分析总结起来有两点。

  1. onInterceptTouchEvent不是每次事件都会被调用的。如果要在代码中提前处理所有的点击事件,可以选择dispatchTouchEvent方法,只有这个方法确保每次都能被调用,而这个前提的保证条件是事件可以传递到当前的ViewGroup
  2. FLAG_DISALLOW_INTERCEPT标识位可以作为解决滑动冲突的一种思路。

ViewGroup不拦截时候,对子View的分发过程

ViewGroup不拦截事件的时候,事件会向下分发给它的子View进行处理。以下是源码实现。

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

以上的代码中,首先遍历这个ViewGroup的子元素,然后判断子元素是否能接受到点击事件。这里是否可以接收到点击事件由两种情况来判定,子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内,如果子元素同时满足这两个条件,那么事件将传递给它处理。
  其中,dispatchTransformedTouchEvent实际上调用的是子元素的dispatchTouchEvent方法,在其内部有一段内容,而又因为其中的子元素不为null,因此会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理,完成一轮事件的分发。

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}

如果子元素的dispatchTouchEvent返回true,那么mFirstTouchTarget就会被赋值同时跳出for循环,代码如下。

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

以上代码完成了mFirstTouchTarget的赋值并终止对子元素的遍历。而如果子元素的dispatchTouchEvnet返回了false,ViewGroup就会把事件分发给下一个元素(如果还有下一个元素的话)。
  以上mFirstTouchTarget的过程是在addTouchTarget内部执行的。从其实现中可以看出,mFirstTarget其实是一种单链表的结构。而mFirstTouchTarget是否被赋值,将直接影响ViewGroup内部的对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列的所有点击事件。

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

遍历所有的子元素后事件都没被合适的处理有两种情况。

  1. ViewGroup没有子元素。
  2. 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent返回了false。在这两种情况下,ViewGroup会自己处理点击事件。

上述情况的代码如下。

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

这里第三个参数的child,从前面的dispatchTransformedTouchEvent实现可知,它会调用super.dispatchTouchEvent(event),而ViewGroup的父类即View类,所以这里就调用了ViewdispatchTouchEvent方法,即点击事件交由View类处理。

View类对点击事件的处理过程

View对点击事件处理过程比较简单,这里的View不包含ViewGroup,以下是dispatchTouchEvent方法,如下所示。

public boolean dispatchTouchEvent(MotionEvent event) {
    // If the event should be handled by accessibility focus first.
    // some code about accessibility
    
    boolean result = false;

    // some code 
    
    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;
        }
    }

    // some code 
    
    return result;
}

View对点击事件的处理比较简单,因为View是一个单独的元素,它没有子元素所以没必要向下传递事件,所以它只能直接处理事件。
  从上面源码可以得知,首先会判断有没有设置OnTouchListener,并且如果OnTouchListener中的onTouch方法返回true,按摩onTouchEvent就不会被调用,所以得知OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。
  接着分析onTouchEvent的实现。先看当View处于不可用状态下的点击事件处理过程,代码如下。

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

从代码中得知,disable状态下的View照样会消耗点击事件。
  接着,如果View设置了代理,那么还会执行TouchDelegateonTouchEvent方法,这个onTouchEvent的工作机制和OnTouchListener看起来相似,代码如下。

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

接下来看onTouchEvent对点击事件的具体处理,代码如下。

 if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
        (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            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();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    // If the post failed, unpress right now
                    mUnsetPressedState.run();
                }

                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;

        case MotionEvent.ACTION_DOWN:
            mHasPerformedLongPress = false;

            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.
            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:
            setPressed(false);
            removeTapCallback();
            removeLongPressCallback();
            mInContextButtonPress = false;
            mHasPerformedLongPress = false;
            mIgnoreNextUpEvent = false;
            break;

        case MotionEvent.ACTION_MOVE:
            drawableHotspotChanged(x, y);

            // Be lenient about moving outside of buttons
            if (!pointInView(x, y, mTouchSlop)) {
                // Outside button
                removeTapCallback();
                if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                    // Remove any future long press/tap checks
                    removeLongPressCallback();

                    setPressed(false);
                }
            }
            break;
    }

    return true;
}

从以上源码中可以得知,只要ViewCLICKABLELONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态。而当ACTION_UP事件产生后,会触发performClick方法,这时如果View设置了onClickListener,那么performClick方法内部会调用它的onClick方法,代码如下。

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

ViewLONG_CLICKABLE属性默认为false,而CLICKABLE属性是否为false要看具体的View,总的来说,就是可点击的View的CLICKABLE为true,不可点击的View为false,,比如Button是可以点击的,但TextView是不可点击的,而通过setClickable和setLongClickable可以分别改变ViewCLICKABLELONG_CLICKABLE属性。另外,setOnClickListenersetOnLongClickListener会自动设置以上两个属性为true。
  到此,事件分发的源码实现已经分析完毕。

你可能感兴趣的:(# View事件分发(二))