Android源码View事件分发和消费详解.md

我们都知道,用户与app进行交互都是通过activity来进行的,而我们平时在activity中设置的view是怎么接收到用户交互的事件呢,activity与view又有怎么样的层级关系呢?我们来看一个很简单的页面:

图一

image

图二

image.png

图三

image.png

从三张图我们可知,我们在activity中通过setContentView设置的view就是图一中的ContentViews部分,它的根布局是FrameLayout,也是图二中的红色区域。图三中的红色区域就是图一中的TitleView。知道了activity与view的层级关系后,那么事件是怎么一步步传到view中的呢?

首先来看Activity中的实现:


/**
 * 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.
 */ public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

大概意思就是,如果Window的superDispatchTouchEvent返回true,表示Activity将该事件传递给Window来处理,如果返回false,则Activity自行处理该事件,Activity的onTouchEvent将会被调用,


/**
 * Called when a touch screen event was not handled by any of the views * under it.  This is most useful to process touch events that happen * outside of your window bounds, where there is no view to receive it. * * @param event The touch screen event being processed.
 * * @return Return true if you have consumed the event, false if you haven't.
 * The default implementation always returns false. */ public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

下面再来看看Window的superDispatchTouchEvent返回true的情况,由于Window的实现类为PhoneWindow,


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

有此可知调用了mDecor,mDecor就是DecorView,是根View,继承至FrameLayout


public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

关于点击事件如何在View中进行分发,点击事件到达顶级view(一般是一个ViewGroup),会调用ViewGroup的dispatchTouchEvent方法,当ACTION_DOWN事件发生后,如果ViewGroup选择拦截该事件,那么接下来的ACTION_MOVE和ACTION_UP等这一事件序列的所有事件都不能被传递给ViewGroup的子View,都交给了ViewGroup进行处理,所以说,ViewGroup不能拦截ACTION_DOWN事件,否则子view将接收不到任何事件。ViewGroup不拦截ACTION_DOWN事件,则交给子View进行出来,接着会调用子view的dispatchTouchEvent事件,如果子view的dispatchTouchEvent返回true表示消费该事件,否则表示不消费该事件(会将该事件又返回给父ViewGroup进行处理)。

[图片上传失败...(image-e9c0ad-1624893293481)]

如果事件交由ViewGroup进行处理,执行ViewGroup的超类View的dispatchTouchEvent方法,这时如果ViewGroup的mOnTouchListener被设置了,就会执行OnTouchListener的onTouch方法,如果onTouch方法返回false,则接着会执行onTouchEvent方法


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;

    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;
    }
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                if ((viewFlags & TOOLTIP) == TOOLTIP) {
                    handleTooltipUp();
                }
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                }
                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:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;

                if (!clickable) {
                    checkForLongClick(0, x, y);
                    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. 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:
                if (clickable) {
                    setPressed(false);
                }
                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
 if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
 // Remove any future long press/tap checks removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }
                break;
        }

        return true;
    }

    return false;
}

onTouchEvent会判断该ViewGroup是否是可点击的,也就是是否设置了mOnClickListener,如果设置了表示消费该事件,onClick将会被调用。否则也就表示不消费该事件,该事件还是会被抛给当前的父View进行处理。当ViewGroup不拦截此事件,会交给子View进行处理,然后再一次执行dispatchTouchEvent方法。到此为止,事件已经从顶级View传递给了下一层的View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。

所以我们接着看ViewGroup的dispatchTouchEvent,

[图片上传失败...(image-72149b-1624893293479)]

当发生ACTION_DOWN事件时,cancelAndClearTouchTargets会将mFirstTouchTarget重置为null,resetTouchState会将mGroupFlags置为0。

从上面代码我们可以看出,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget != null,ACTION_DOWM 好理解,那么mFirstTouchTarget是什么呢,由后面的代码可知,mFirstTouchTarget在ViewGroup将事件传递给子view时,mFirstTouchTarget会被赋值并被指向子元素。


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

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

也就是当ViewGroup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget != null。反过来,一旦ViewGroup需要拦截此事件,mFirstTouchTarget != null就不成立了。那么当ACTION_UP和ACTION_MOVE事件到来时,actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null就不成立了,将导致ViewGroup的onInterceptTouchEvent不会被调用,intercepted为true,并且同一事件序列的其他事件也默认交给ViewGroup处理。

当然,这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法设置的,


/**
 * Called when a child does not want this parent and its ancestors to * intercept touch events with * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
 * * 

This parent should pass this call onto its parents. This parent must obey * this request for the duration of the touch (that is, only clear the flag * after this parent has received an up or a cancel.

** @param disallowIntercept True if the child does not want the parent to * intercept touch events. */ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);

一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的点击事件。为什么说出了ACTION_DOWN以外其他点击事件,是因为发生ACTION_DOWN事件时,会重置mGroupFlags标记位,还会置mFirstTouchTarget为null,将导致子View设置的标记位失效。因此当发生ACTION_DOWN事件时,ViewGroup总是询问自己是否要拦截事件,从上面代码就可以看出。当ViewGroup决定拦截事件时,后续这一事件序列里的所有事件都默认交给ViewGroup自己处理并且不再调用onInterceptTouchEvent方法。设置mGroupFlags为FLAG_DISALLOW_INTERCEPT的作用就是让ViewGroup不再拦截事件,前提时ACTION_DOWN事件没有被ViewGroup拦截。

你可能感兴趣的:(Android源码View事件分发和消费详解.md)