美文网首页
事件传递机制

事件传递机制

作者: 慎独静思 | 来源:发表于2022-03-17 00:06 被阅读0次

有人问你,能详细介绍一下事件传递机制吗?
事件传递过程中涉及到的主要类有:Activity,ViewGroup,View,主要方法有:dispatchTouchEvent, onTouchEvent, onInterceptTouchEvent, requestDisallowInterceptTouchEvent,涉及到的事件主要有:ACTION_DOWN, ACTION_MOVE, ACTION_UP
一个事件最先到达Activity,再到ViewGroup,最后到View,如果有人消费了事件,则事件不会往回传递了,否则事件会回到Activity,呈一个U行传递。如果ViewGroup的onInterceptTouchEvent返回true,则会拦截事件,事件不会传递到view,View也可以通过调用requestDisallowInterceptTouchEvent禁止ViewGroup拦截事件,了解事件传递对解决滑动冲突非常有帮助。

但这里有太多的疑问了,比如:
1、Activity的事件是谁传给它的?
2、Down, Move,Up分别是怎么传递的?
3、Fragment在这个过程中起到什么作用了吗?
4、Activity的事件是怎么给到View的?
5、窗口的层级关系,window,ViewRootImpl,View什么关系,怎么分层?
6、滑动冲突产生的原因,怎么解决?

咱们先详细了解整个过程,再一一解答这些疑问。

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

Activity的dispatchTouchEvent,目前我们知道的事件处理是从这里开始的。
咱们都知道Activity的mWindow其实是PhoneWindow,所以看PhoneWindow的superDispatchTouchEvent

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

DecorView其实是一个ViewGroup,它是一个根ViewGroup,是在setContentView时创建的,平时我们使用的android.R.id.content就被添加到DecorView中,而咱们自己写的布局是加在content上的。

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

所以,DecorView的superDispatchTouchEvent最终会调用ViewGroup的dispatchTouchEvent方法。
下面咱们看一下dispatchTouchEvent的代码:
第一部分

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

上边的代码用来判断是否拦截事件,如果不是Down事件并且touch target为空,则拦截事件,事件由自己处理。mFirstTouchTarget != null后边会详细说。

@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

咱们可以调用requestDisallowInterceptTouchEvent来避免ViewGroup拦截事件。

public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

你可以重写onInterceptTouchEvent来决定是否拦截事件。
Down 事件到来时,如果此方法返回true,则mFirstTouchTarget不会被赋值,那么后续的事件都会被拦截,onInterceptTouchEvent只会被回调一次。

下面继续看dispatchTouchEvent的代码。
接下来分两种情况:ViewGroup拦截和不拦截

先看不拦截的情况

                        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;
                            }
                            // 1、判断子View是否满足条件
                            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);
                            // 2、事件分发给目标子view 
                            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 (preorderedList != null) preorderedList.clear();
                    }

上边的代码遍历ViewGroup的子view,查找处理事件的子View。
主要分为两部分

1、判断子view是否能够接收事件
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
/**
     * Returns true if a child view can receive pointer events.
     * @hide
     */
    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }

判断子view是否正在播放动画,如果是则跳过。

/**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

判断点击事件是否落在子view的范围内。
如果点击事件落在子view范围内,且没有动画正在执行,则可接收事件,继续看第二部分。

2、事件分发给子view
                            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;
                            }

方法dispatchTransformedTouchEvent中如果参数child为空,则调用ViewGroup的dispatchTouchEvent方法处理事件,如果child不空,则调用child的dispatchTouchEvent方法处理事件。
如果child的处理当前事件,则mFirstTouchTarget会被赋值,并跳出循环,否则继续查找下一个满足条件的子view。如果没找到,它会自己处理事件。
从上边拦截部分的讲解可知,如果mFirstTouchTarget为空,则ViewGroup会拦截接下来的全部事件。
到这里事件就由父view传到了子view。

再看拦截的情况

如果ViewGroup拦截事件,则mFirstTouchTarget为空

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

dispatchTransformedTouchEvent方法传child为空,则调用super#dispatchTouchEvent,其实就是View#dispatchTouchEvent方法。

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

以上就是ViewGroup部分的事件处理流程,接下来看View部分的分析
View部分的逻辑比较简单

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

在dispatchTouchEvent方法中会先判断View是否设置了OnTouchListener,如果设置了并且onTouch返回true,则会屏蔽View的onTouchEvent回调,否则执行onTouchEvent

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

首先判断当前组件是否可点击,如果是可点击的,即使现在是disable状态,事件仍然会被消费。
如果组件不是clickable的,那事件不会被消费。

                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;

在DOWN事件中主要是设置按压状态,开始长按倒计时等操作。

                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;

MOVE时主要检查手指当前是否仍在View中。

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

                        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;

在Up事件中,主要是状态的设置和操作的执行,如果长按事件已经执行,则不在执行click操作,否则执行click处理。

    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

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

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

执行点击操作的逻辑也比较简单。
到这里事件分发基本流程就结束了。

下面总结一下:
Down事件下发之后,在ViewGroup会先进行判断是否拦截,如果拦截,则后续的Move, Up事件均会被拦截,事件走它自己的处理逻辑。如果不拦截,则事件达到子view中,如果子view是可点击的,则会消耗事件。后续的Move,Up事件中ViewGroup仍然可决定是否拦截。如果子view不处理,则回到ViewGroup处理,如果ViewGroup不处理,继续回到父ViewGroup处理,直到Activity。
如果子view在Down事件返回false,那么后续的MOVE和UP均无法收到。
Move和Up事件的分发和Down事件类似,这里不再展开分析。

1、Activity的事件是谁传给它的?

image.png

参考:https://juejin.cn/post/6918272111152726024

2、Down, Move,Up分别是怎么传递的?

image.png
图片来源 http://gityuan.com/2015/09/19/android-touch/

3、Fragment在这个过程中起到什么作用了吗?

Fragment在事件分发过程中没起作用,Fragment的布局也就是在onCreateView中创建的View会被添加到Fragment在Activity的容器View中,这样就正常被添加到View tree中,可以正常接受分发的事件。

// 1、FragmentManager                  
f.mContainer = container;
// 这里调用onCreateView给mView赋值
f.performCreateView(f.performGetLayoutInflater(
        f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
    f.mInnerView = f.mView;
    f.mView.setSaveFromParentEnabled(false);
    if (container != null) {
        container.addView(f.mView);
    }

// 2、FragmentController
/**
     * Attaches the host to the FragmentManager for this controller. The host must be
     * attached before the FragmentManager can be used to manage Fragments.
     */
    public void attachHost(Fragment parent) {
        mHost.mFragmentManager.attachController(
                mHost, mHost /*container*/, parent);
    }

// 3、Activity中创建
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

4、Activity的事件是怎么给到View的?

这个上边已经说到了,Activity -> PhoneWindow -> DecorView -> ViewGroup->View

5、窗口的层级关系,window,ViewRootImpl,DecorView什么关系,怎么分层?

在调用WindowManager#addView时会创建ViewRootImpl,这里addView其实是添加的DecorView,并使DecorView和ViewRootImpl产生关联。

6、滑动冲突产生的原因,怎么解决?

滑动冲突的场景:
1、外部滑动和内部滑动方向不一致。
2、外部滑动和内部滑动方向一致。
3、1和2两种情况的嵌套。

解决方法:外部拦截法(推荐)和内部拦截法

外部拦截法:
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器处理事件) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }
        return intercepted;
    }

ACTION_DOWN事件不能拦截,否则子view收不到后续的MOVE和UP事件了。

内部拦截法需要父View和子View配合修改
// 父View 修改部分
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return false;
        }
        
        return true;
    }

父View默认拦截除ACTION_DOWN之外的全部事件。因为ACTION_DOWN不受requestDisallowInterceptTouchEvent的控制。

// 子View修改部分
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要处理事件) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

子View通过requestDisallowInterceptTouchEvent禁止父View拦截事件。如果父View需要处理事件,则父View拦截事件。

参考:《Android开发艺术探索》等

相关文章

  • 深入浅出iOS事件机制

    深入浅出iOS事件机制事件传递:响应链事件传递响应链

  • 安卓事件传递机制

    事件传递机制 View dispatchTouchEvent():分发事件 onTouchEvent():处理事件...

  • 01进阶之路-UI视图

    1. 事件传递机制和响应者链条 学习链接 事件传递机制iOS中的事件可以分为3大类型 1 触摸事件 2 加速计...

  • 事件传递机制

    主要内容 理论部分 常见应用 理论部分 iOS中事件(UIEvent)主要是以下几种,本文主要是分析触控事件(UI...

  • 事件传递机制

    ActivitydispatchTouchEvent 返回true false 将会在自己的dispatchTou...

  • 事件传递机制

    一般来说:Activity------>Window--------->DevorView(setContentV...

  • 事件传递机制

    事件传递机制传送门 http://note.youdao.com/noteshare?id=2cca71064b9...

  • 事件传递机制

    View接收到事件,如果view不能处理该事件,并且她不是顶层的view,则会将事件往他的父view进行传递; 父...

  • 事件传递机制

    有人问你,能详细介绍一下事件传递机制吗?事件传递过程中涉及到的主要类有:Activity,ViewGroup,Vi...

  • Android触摸事件的应用

    前言 上一篇讲了Android触摸事件的传递机制,具体可以看这里 初识Android触摸事件传递机制。既然知道A...

网友评论

      本文标题:事件传递机制

      本文链接:https://www.haomeiwen.com/subject/nfffdrtx.html