Android事件分发机制

文章目录

      • Android View事件分发机制:
        • 事件分发中的核心方法
        • onTouchListener和onClickListener的优先级
        • 事件分发
          • DOWN,MOVE,UP 事件分发
          • CANCEL
        • 代码实践
        • requestdisallowIntereptTouchEvent作用

Android View事件分发机制:

事件分发中的核心方法

Android中事件分发,实际上分发的是MotionEvent,事件分发的过程中,涉及到下面三个核心的方法:

  • dispatchTouchEvent:用来进行事件的分发,只要事件传递给当前View,那么这个方法一定会被调用,该方法的返回值受到onTouchEvent和子View的dispatchTouchEvent的影响。
  • onInterceptTouchEvent:用来询问是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,这个方法不会被再次调用。(onInterceptTouchEvent只存在于ViewGroup中,Activity和普通的View中都没有这个方法)
  • onTouchEvent:用来处理MotionEvent
    上面的方法,如果返回的是true,就表示消耗当前的事件。

onTouchListener和onClickListener的优先级

onTouchListener 优先级高于 onClickListener
onTouchListener优先级高于onClickListener,onTouchListener返回false,后续的click事件才会被处理,onTouchListener返回true表示消耗了事件,不会再传递。

事件分发

事件传递的时候是由Activity->window->view,如果view不处理的话,最后事件会回到activity,在事件的流程中:View不会分发事件,View只会处理事件,ViewGroup会先分发事件,如果子View没有处理事件,尝试自己处理事件,如果自己没有处理,最后交给Activity。

DOWN,MOVE,UP 事件分发

android.view.ViewGroup#dispatchTouchEvent

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            // 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);
				// 一次事件开始,ViewGroup会先清除之前的状态,例如清楚:FLAG_DISALLOW_INTERCEPT的标记
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            // 为down事件或者mFirstTouchTarget不为空表示找到了消耗touch事件的view
            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的所有child,寻找是否需要消耗事件
			// Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        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;
                            }
							// canReceivePointerEvents判断view是否可见&view没有再播放动画,
							// isTransformedTouchPointInView判断点击区域是否在view的范围内
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

ViewGroup的TouchEvent中,都会先去判断View是否有子view,有子view的话递归调用子View的dispatchTouchEvent方法,否则直接调用自己的dispatchTouchEvent方法:

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

Down事件分发后,一定会找到了消耗事件的view(或者事件直接被Activity消耗),如果ViewGroup没有消耗事件,事件就不会再继续往当前ViewGroup分发,UP和MOVE事件都会发送到DOWN事件的消耗这上,mFirstTouchTarget不为null,直接找到target进行分发。

           // 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);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

View的dispatchTouchEvent
android.view.View#dispatchTouchEvent

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

View的dispatchTouchEvent中,会先判断view有没有设置TouchListener,如果设置了TouchListener再去判断onTouch方法的返回值,如果onTouch方法没有消耗事件,会再调用onTouchEvent方法。如果View 的onTouchEvent方法返回了false,就表示事件没有被消耗,那么最终会调用到ViewGroup的onTouchEvent。

         // 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中的下面的地方,进行事件分发:
		if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);

mFirstTouchTarget为null表示子view没有消耗事件,这里会再调用dispatchTransformedTouchEvent去派发。
ViewGroup事件派发结束后,就会回到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;
        }
        // 执行activity的onTouchEvent
        return onTouchEvent(ev);
    }

如果ViewGroup的onTouchEvent返回false,那么就会调用Activity的onTouchEvent。

CANCEL

Cancel事件触发场景案例:当ScrollView中添加自定义View时,ScrollView在DOWN事件不会进行拦截,当手指滑动到一定的距离后,onInterceptTouchEvent方法返回true,并触发ScrollView的滚动效果,当ScrollView进行滚动的时候,内部的子View会收到一个cancel事件,并丢失焦点。

代码实践

代码地址:https://gitee.com/lxd15130140362/lxd-android-start/tree/master/app/src/main/java/com/example/androidstart/view
界面布局样式:
Android事件分发机制_第1张图片

  1. 都不处理事件,点击自定义的Textview日志顺序如下:
2023-02-19 20:50:19.786 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart D/CustomizeLayout: onInterceptTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart I/CustomizeTextView: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart I/CustomizeTextView: onTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.829 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:50:19.829 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:50:19.927 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:50:19.928 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:50:20.017 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:50:20.017 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:50:20.018 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_UP
2023-02-19 20:50:20.018 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_UP

Down事件会从activity->viewgroup->view,move和up事件都被activity自己消费了,不会进行事件分发,因为之前的down事件没有人分发,就表示子view不会处理点击事件。
事件传输流程:
Android事件分发机制_第2张图片

  1. 自定义的ViewGroup的onInterceptTouchEvent返回true,但是并不消耗事件,日志如下:
2023-02-19 20:57:04.896 7618-7618/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.898 7618-7618/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.898 7618-7618/com.example.androidstart D/CustomizeLayout: onInterceptTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.898 7618-7618/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.899 7618-7618/com.example.androidstart I/Activity: onTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.995 7618-7618/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:57:04.996 7618-7618/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:57:04.998 7618-7618/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:57:04.998 7618-7618/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:57:04.999 7618-7618/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_UP
2023-02-19 20:57:04.999 7618-7618/com.example.androidstart I/Activity: onTouchEvent: ACTION_UP

Down事件会从Activity->ViewGroup,因为ViewGroup进行了拦截,所以这里不会分发到子View,MOVE和UP事件也只会在activity中进行分发

  1. viewGroup的onInterceptTouchEvent调用父类实现,但是onTouchEvent返回truue,即ViewGroup不拦截但是消耗事件。
2023-02-19 21:17:28.939 9448-9448/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.940 9448-9448/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.941 9448-9448/com.example.androidstart D/CustomizeLayout: onInterceptTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.941 9448-9448/com.example.androidstart I/CustomizeTextView: dispatchTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.942 9448-9448/com.example.androidstart I/CustomizeTextView: onTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.942 9448-9448/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_DOWN
2023-02-19 21:17:29.021 9448-9448/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 21:17:29.022 9448-9448/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_MOVE
2023-02-19 21:17:29.022 9448-9448/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_MOVE
2023-02-19 21:17:29.023 9448-9448/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_UP
2023-02-19 21:17:29.023 9448-9448/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_UP
2023-02-19 21:17:29.023 9448-9448/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_UP

down事件会activity->viewgroup->view,因为viewgroup消耗了事件,因此down事件不会回到activity,同时由于已经有了事件消费者,因此MOVE和UP事件不会再往view进行传递,回直接调用到ViewGroup的onTouch中中。

requestdisallowIntereptTouchEvent作用

子view在自己的down或者move的时候调用requestdisallowIntereptTouchEvent,这样父view在这次事件传递中就不会拦截当前链路的事件。
链接:
requestDisallowInterceptTouchEvent失效的原因及解决

你可能感兴趣的:(android,android,java,android,studio)