事件处理机制原理分析

132 阅读7分钟

事件处理机制原理分析

根据 Android12(SDKVersion:32)源码进行分析的

View的继承关系

图片.png

事件处理机制的几个重要方法

  1. dispatchTouchEvent
  2. onInterceptTouchEvent
  3. onTouchEvent

图片.png

TouchEvent 事件传递

图片.png

由日志文件可得 :

ViewGroup 容器: dispatchTouchEvent 是由最底层 向 上层 分派 事件的,分派到最上层时 调用 view的onTouchEvent事件进行处理(递归传递)。如果 dispatchTouchEvent函数 return true/false 事件就不会往上传递了,return super.dispatchTouchEvent(...) 才会往上传。

ViewGroup 也可以使用 onInterceptTouchEvent 事件拦截回调方法 return true,拦截后,就可以在 onTouchEvent 方法中对事件进行处理

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true; // 拦截,事件不会往上传递
        // return false; //不拦截,事件往上传递
    }

	@Override
    public boolean onTouchEvent(MotionEvent event) {
        // 处理事件
    }

View 的 dispatchTouchEvent、 onTouchEvent 都是从 最上层 往下 传递事件

所以 onTouchEvent 事件,是从 最上层 往下 传递事件

事件传递全流程-源码分析

带着以下问题看源码

1、onTouch 和 onClick 的关系 与 执行的位置 2、onTouchEvent 在哪儿执行的 3、LongClick 事件触发 4、按下移出 View,为什么 onClick 不执行?

dispatchTouchEvent@Activity.java
	getWindow()@PhoneWindow.superDispatchTouchEvent(ev)
		mDecor.superDispatchTouchEvent(event);
			super.dispatchTouchEvent(event);
				ViewGroup.dispatchTouchEvent    // 事件分发机制
					// 这里就能解释为什么 view 的事件传递是 从上往下传递
					for (int i = childrenCount - 1; i >= 0; i--) {
						dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
							child@View.dispatchTouchEvent   //事件处理方法
								// 如果设置了 OnTouchListener,而且返回true
								if (li != null && li.mOnTouchListener != null
                                        && (mViewFlags & ENABLED_MASK) == ENABLED
                                        && li.mOnTouchListener.onTouch(this, event)) {
                                    result = true;
                                }
								// result 为 true 就不会执行 onTouchEvent
                                if (!result && onTouchEvent(event)) {
                                    result = true;
                                }
					}

// onTouchEvent 在 child@View.dispatchTouchEvent 里面执行
onTouchEvent@View.java
	MotionEvent.ACTION_DOWN: // 按下屏幕
		checkForLongClick // 长按事件,延时操作400ms, 如果400ms内 抬起手或者移出到view外 长按事件就会被移除
	MotionEvent.ACTION_UP: // 抬起手指,离开屏幕
		boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
		if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 查看标识是否能执行点击事件
			removeLongPressCallback(); // 移除长按事件
            performClickInternal();//onclick事件是在onTouchEvent事件里面处理的
                li.mOnClickListener.onClick(this)
		}
	MotionEvent.ACTION_MOVE: // 手指在屏幕上移动
		if (!pointInView(x, y, touchSlop)) {//表示 touch事件 移出了view
			removeLongPressCallback(); // 移除长按事件
			setPressed(false); // 设置标识,让点击事件失效
		}

结论

  1. ViewGroup的事件分发是 从下往上分发的,View 是从上往下消费事件的
  2. 如果view 同时设置了 onTouch事件和 onClick事件,如果onTouch事件里返回了true,onClick事件、长按事件都不会执行。

图片.png

同时设置 onTouch 和 onClick - 实战代码

tv.setOnTouchListener((v, event) -> {
    Log.d("hehe", "onTouch: "+event.getAction());
    //            return true; // 不会执行onCLick
    return false; // return false 且 没移动到view外面的话 会执行 onClick
});
tv.setOnClickListener(v -> {
    Log.d("hehe", "OnClick: "+v.getAccessibilityClassName());
});

log: 图片.png

dispatchTouchEvent 函数详解

父容器分发流程 》 递归 》 责任链模式

MotionEvent.ACTION_DOWN // 只执行一次,手指初次点击时执行
MotionEvent.ACTION_UP // 只执行一次,手指最后抬起时执行
MotionEvent.ACTION_POINTER_DOWN // 多指操作 按下标识
MotionEvent.ACTION_POINTER_UP // 多指抬起标识


第一根手指按下:ACTION_DOWN
第二根 - 第n-1根:ACTION_POINTER_DOWN

抬起:最后一根手指之前:ACTION_POINTER_UP
抬起:最后一根手指:ACTION_UP

源码分析

// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    // If the event targets the accessibility focused view and this is it, start
    // normal event dispatch. Maybe a descendant is what will handle the click.
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

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

        // 检查是否拦截 -- 父容器的权利.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            // disallowIntercept 决定了 onInterceptTouchEvent 会不会执行
            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;
        }

        // If intercepted, start normal event dispatch. Also if there is already
        // a view that is handling the gesture, do normal event dispatch.
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // Check for cancelation.
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // Update list of touch targets for pointer down, if needed.
        final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
        // 是否支持多指,默认为 true
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                && !isMouseEvent;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        // 在 if 中分发事件
        if (!canceled && !intercepted) { // 没被取消和拦截,就分发事件
            // If the event is targeting accessibility focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            // 第一根手指按下
            if (actionMasked == MotionEvent.ACTION_DOWN
                	// 第 n(n>1) 根手指按下
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                	// 鼠标移动事件
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // 表示手指按下顺序的下标,ACTION_DOWN , actionIndex = 0
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                // 手指的id,用于识别是那根手指在操作 int(4个字节,32位),1位代表1根手指
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);

                // 有多少个孩子节点
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x =
                            isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                    final float y =
                            isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    // 将子view 进行排序(按z轴 由低到高)
                    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;
                        }

                        // 是否能处理点击事件
                        if (!child.canReceivePointerEvents()
                            	// 判断点击区域是否在 view上
                                || !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);
                        // 询问 child 是否处理事件,如果child处理,则命中if -- 递归
                        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(链表结构) = mFirstTouchTarget
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 表示child 处理了事件
                            alreadyDispatchedToNewTouchTarget = true;
                            // 如果 dispatchTransformedTouchEvent return true,
                            // 表示child处理了事件,退出循环,不再访问其他child
                            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();
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // Dispatch to touch targets.
        // 没有child处理事件的时候
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            // 询问自己是否处理
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
            // handled = super@View.dispatchTouchEvent(transformedEvent);
        } 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;
                // 表示 已经有child处理了事件,直接返回 handled,不作处理
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    // 询问 target.child 是否处理
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // 如果 cancle为 true,取消child处理事件
                    if (cancelChild) {
                        if (predecessor == null) {
                            // mFirstTouchTarget 置为 null(如果是单指的情况)
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

Android 最多识别多少手指?

// 手指的id,用于识别是那根手指在操作 int(4个字节,32位),1位代表1根手指 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;

实例分析

自定义view 重叠时,冲突是必然的,就需要根据需求来处理冲突

ViewGroup才分发事件(最底层往上分发),View只消费事件(最上层往下消费)

处理冲突1、内部拦截法(由子view根据条件 决定事件 由谁处理);2、外部拦截法(由父容器决定)

ViewPager + ListView的组合

默认情况下,只能上下滑动,ViewPager父容器是默认(onInterceptTouchEvent{return false})不拦截事件的,ListView接收到事件 会进行处理

如果 ViewPager 重写了 onInterceptTouchEvent 并且 return true 事件就不会分发到子view。UI情况就是 只能 ViewPager的左右滑动,不能上下滑动

问题:现在假设 ViewPager 除了 action_down 能分发事件之外,其他都被拦截了,在ListView 中使用内部拦截法来解决问题?

parent.requestDisallowInterceptTouchEvent(true); // 这个方法能使 parent 里面的 onInterceptTouchEvent 失效

所以内部拦截法解决问题的思路是:

  1. ViewPager 的action_down 分发事件给 ListView之后,
  2. ListView 在 action_down 事件里面 调用parent.requestDisallowInterceptTouchEvent(true); 使得ViewPager 的onInterceptTouchEvent 失效
  3. 在 ListView里面的 action_move 里面 按需求判断 是否让 ViewPager 拦截事件,以此解决事件冲突问题。

外部拦截法思路:

在ViewPager 的onInterceptTouchEvent 里面决定是否拦截事件,拦截的话就ViewPager自己处理事件(可以左右滑动),不拦截的话就分发事件给 ListView,ListView是默认处理事件的(可以上下滑动)