Android View的事件分发(三)-事件的分发(dispatchTouchEvent)

1,125 阅读7分钟

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战

相关文章:
Android View的事件分发(一)-事件分发的过程
Android View的事件分发(二)-事件传递顺序
Android View的事件分发(三)-事件的分发(dispatchTouchEvent)
Android View的事件分发(四)-事件处理(onTouchEvent)
Android View的事件分发(五)-事件拦截(onInterceptTouchEvent)

上一篇文章,介绍了View事件处理的传递顺序,已经整个过程中事件处理的传递和回调,怎么处理,在哪里处理,通过流程图可以看得很清楚,但是,要想详细的了解整个过程的原理,还是需要通过源码的分析,才知道,那么下面就介绍dispatchTouchEvent 的源码,看看它是怎么分发事件的。

DispatchTouchEvent

自上而下的分发,先从Activity的dispatchTouchEvent看:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

调用的是super也就是父类的dispatchTouchEvent:

/**
     * 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 的任何一处点击或者滑动都会响应,一般不使用。接下去看getWindow().superDispatchTouchEvent(ev) 所代表的具体含义。getWindow() 返回对应的 Activity 的 window。一个Activity 对应一个 Window 也就是 PhoneWindow, 一个 PhoneWindow 持有一个 DecorView 的实例, DecorView 本身是一个 FrameLayout。这句话一定要牢记。

父类的实现中,有一个onUserInteraction方法,这个方法主要是响应用户在界面的点击或者滑动,一般不会使用,下面看getWindow().superDispatchTouchEvent(ev),getWindow很明显,是返回Activity 的 window对象,一个Activity 对应一个 Window 也就是 PhoneWindow, 一个 PhoneWindow 持有一个 DecorView 的实例, DecorView 本身是一个 FrameLayout,这个可能是面试过程中会问的,最好记住,哈哈

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

在PhoneWindow找到上面的方法:

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

这个方法又调用了父类的superDispatchTouchEvent,PhoneWindow的父类,就是DecorView,也就是根View了,我们调用setContentView,设置的就是它的子View这个在前面的View的绘制流程中已经说过,这里不多说了,到这里事件已经被传递到根 View 中,而根 View 其实也是 ViewGroup,所以最后走到的是ViewGroup的事件分发了,那么看下ViewGroup的事件分发。

ViewGroup 事件分发

public boolean dispatchTouchEvent(MotionEvent ev) {
            ......

            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
       // 当有 down 操作,会把之前的target 以及标志位都复位
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);

                //清除 FLAG_DISALLOW_INTERCEPT,并且设置 mFirstTouchTarget 为 null
                resetTouchState(){
                    if(mFirstTouchTarget!=null){mFirstTouchTarget==null;}
                    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
                    ......
                };
            }
            final boolean intercepted;//ViewGroup是否拦截事件

             // mFirstTouchTarget是ViewGroup中处理事件(return true)的子View
            //如果没有子View处理则mFirstTouchTarget=null,ViewGroup自己处理
            if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//onInterceptTouchEvent
                    ev.setAction(action);
                } else {
                    intercepted = false;

                    //如果子类设置requestDisallowInterceptTouchEvent(true)
                    //ViewGroup将无法拦截MotionEvent.ACTION_DOWN以外的事件
                }
            } else {
                intercepted = true;

                //actionMasked != MotionEvent.ACTION_DOWN并且没有子View处理事件,则将事件拦截
                //并且不会再调用onInterceptTouchEvent询问是否拦截
            }

            ......
            ......
}

上面代码里有个标志位:mFirstTouchTarget,在if条件里,2个条件,意思是当按下事件触发后,或者有子View处理了事件,再到里面,看判断条件,当子View没有调用requestDisallowInterceptTouchEvent来拦截ViewGroup 的拦截,这句话有点绕哈,意思就是子View不做请求,由ViewGroup处理,那么 ViewGroup 的 onInterceptTouchEvent 就会被调用,来判断是否是要拦截。当子 view 调用requestDisallowInterceptTouchEvent,请求不允许拦截事件(字面意思哈),即使父 View的onInterceptTouchEvent 中返回true 也没用了。

这里需要注意的就是:onInterceptTouchEvent 默认返回 false。 当 ACTION_DOWN 事件到来时,此时 mFirstTouchTarget 为 null,此时其实也还未收到子 view requestDisallowInterceptTouchEvent。所以这时候,只要父 view 把 ACTION_DOWN 事件给拦截了,那么子 view 就收不到任何事件消息了。所以,一般在 ACTION_DOWN 的时候,父 view 不作拦截。

所以如果子View想父类不拦截事件的处理,就调用requestDisallowInterceptTouchEvent,让子View来处理相应的MotionEvent。

FLAG_DISALLOW_INTERCEPT 这个标记位就是通过子 View requestDisallowInterceptTouchEvent 方法设置的。

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

如果这个 ViewGroup 有父 View 的时候,还得让父父 View 不能拦截。mParent.requestDisallowInterceptTouchEvent(disallowIntercept);

public boolean dispatchTouchEvent(MotionEvent ev) {
        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 (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) 
            {
                ev.setTargetAccessibilityFocus(false);
                //如果子View没有播放动画,而且点击事件的坐标在子View的区域内,继续下面的判断
                continue;
            }
            //判断是否有子View处理了事件
            newTouchTarget = getTouchTarget(child);

            if (newTouchTarget != null) {
                //如果已经有子View处理了事件,即mFirstTouchTarget!=null,终止循环。
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                //点击dispatchTransformedTouchEvent代码发现其执行方法实际为
                //return child.dispatchTouchEvent(event); (因为child!=null)
                //所以如果有子View处理了事件,我们就进行下一步:赋值

                ......

                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                //addTouchTarget方法里完成了对mFirstTouchTarget的赋值
                alreadyDispatchedToNewTouchTarget = true;

                break;
            }
        }
    }

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

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ......

            if (child == null) {
            //如果没有子View处理事件,就自己处理
                handled = super.dispatchTouchEvent(event);
            } else {
           //有子View,调用子View的dispatchTouchEvent方法
                handled = child.dispatchTouchEvent(event);

            ......

            return handled;
    }

所以需要哪层处理事件,就让父类不拦截,不需要怎么默认是父类处理,如果有子View,则调用子View的dispatchTouchEvent方法判断当前事件是否处理了,如果处理了则给赋值 mFirstTouchTarget,赋值成功则跳出循环。ViewGroup的事件分发,最终还是调用了View的dispatchTouchEvent,代码里可以清楚的看到。分发是一层层由上而下,回调结果则是由下而上,直到回到顶层,或者被消费了,也就是处理了,上一篇的文章的流程图可以清晰的看到。

源码还是很清晰的,包括命名

View最终是怎么去处理事件的

class View:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

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

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

上面是View的dispatchTouchEvent方法的全部代码。很明显View是单独的一个元素,它没有子View,所以也没有分发的代码。我们需要关注的也只是上面当中的一部分代码。

        //如果窗口没有被遮盖
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            //当前监听事件
            ListenerInfo li = mListenerInfo;
            //需要特别注意这个判断当中的li.mOnTouchListener.onTouch(this, event)条件
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //result为false调用自己的onTouchEvent方法处理
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

通过上面代码我们可以看到View会先判断是否设置了OnTouchListener,如果设置了OnTouchListener并且onTouch方法返回了true,那么onTouchEvent不会被调用。 当没有设置OnTouchListener或者设置了OnTouchListener但是onTouch方法返回false则会调用View自己的onTouchEvent方法。

至此,事件的分发介绍的差不多了,从Activity的分发,到Window(这里是PhoneWindow),再到DecorView最后到ViewGroup,就是整个事件分发的过程,当然最后还是调用的View的dispatchTouchEvent处理的。是个清晰,自然的分发过程。有介绍有误的,还请指正,欢迎留言评论。