Principle and source code analysis of Android event distribution mechanism of advanced UI

foreword

In Android, the event distribution mechanism is a very important knowledge point. Mastering this mechanism can help you solve many View event conflicts in your usual development. This question is also a question that is often asked in interviews . This article will summarize this knowledge point.

event distribution reason

The View on the page in Android is displayed in a tree structure, and the View will overlap. When there are multiple Views that can respond to the place we click, who should the click event be given to? In order to solve this problem, an event distribution is required. mechanism

event distribution object

Touch event, that is, to pass each Touch event (MotionEvent) to View. As for whether the event is finally processed or not, it depends on the logic of the receiver

When the user touches the screen, a Touch event will be generated (the Touch event is encapsulated into a MotionEvent object), which is mainly divided into the following types

  • MotionEvent.ACTION_DOWN: The moment you tap the screen with your finger, this event is generated, which is the start of all events
  • MotionEvent.ACTION_MOVE: This event is generated when the finger slides on the screen
  • MotionEvent.ACTION_CANCLE: End the current event for non-human reasons
  • MotionEvent.ACTION_UP: This event is generated as soon as the finger leaves the screen

A complete Touch event is the process from the user's finger touching the screen (accompanied by an ACTION DOWN event) to the user's finger leaving the screen (accompanied by an ACTION UP event). The whole process is as follows

ACTION DOWN (once) --> ACTION MOVE (N times) --> ACTION_UP (once)

Event dispatch method

  • dispatchTouchEvent(MotionEvent ev): It can be seen from the method name that its function is to distribute events; when an event is detected by the underlying driver, it will be reported, and finally it will be handled by the method of the Activity to decide by itself Consumption continues to pass on
  • onInterceptTouchEvent(MotionEvent ev): When an event is distributed to ViewGroup, it can decide whether to intercept the event, this method is only owned by ViewGroup
  • onTouchEvent(MotionEvent event): This is the last method of the event distribution process, that is, whether to consume the event

Event Distribution Participants

  • Activity: Contains ViewGroup and View
  • ViewGroup: Contains ViewGroup and View
  • View: does not contain other Views, only itself

The event distribution flow is generally Activity --> ViewGroup --> … --> View

Notice:

  • The child View can intervene in the event distribution process of the parent View (except the ACTION_DOWN event) through the requestDisallowInterceptTouchEvent method, and this is the key method we often use to deal with sliding conflicts
  • If View sets onTouchListener and returns true in the overridden onTouch method, then its onTouchEvent method will not be called, because onTouch is executed prior to onTouchEvent in View's dispatchTouchEvent; the onClick method will not be called either, because onClick is in callback in onTouchEvent

Event Distribution Process

  1. When the finger touches the screen, the underlying Input driver reads and writes the hardware input device node named event[NUMBER] from the /dev/input/ path to obtain events (you can view the nodes under your device through adb shell getevent, and Android also reads and writes from these The node obtains these raw data and then packages them for developers to use; if you are doing game development, you may directly obtain these raw data and process them yourself), after a series of calls, it is passed to the dispatchTouchEvent method of DecorView
  2. In DecorView, the event will continue to be passed through the internal interface Callback of Window, because the Activity implements this interface, and the story piece is distributed to the Activity; after the Activity obtains the event, it first distributes the event to the window where the Activity is located in the dispatchTouchEvent method , the actual type is PhoneWindow, and this window hands the event to its top-level view, namely DecorView, for processing
  3. DecorView is a subclass of FrameLayout, that is, a subclass of ViewGroup. It does not handle it by itself, but continues to hand over the event to ViewGroup; just such an event is transferred from Activity to ViewGroup
  4. ViewGroup distributes in the dispatchTouchEvent method. If its own onInterceptTouchEvent method intercepts this event, it will hand over the event to its own onTouchEvent method for processing; otherwise, traverse its own sub-Views and continue to distribute events, as long as one sub-View consumes the event , then stop traversing
  5. The event will be passed to the dispatchTouchEvent method of the sub-View. If the OnTouchListener is registered for the sub-View and true is returned, the event distribution will end here; otherwise, the event will continue to be passed to the onTouchEvent method of the sub-View
  6. The sub-View will call back the View's onClick listener in the ACTION_UP event. If the sub-View does not consume this event, it will pass it back to the Activity according to the distribution process; if no one has consumed it (including the Activity itself), it will destroy this event

Event distribution source code

The following source code is based on API24

Corresponding to the above process, when there is a Touch event, the steps are as follows

DecorView.dispatchTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
    
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

The cb here refers to the Callback interface inside the window. The Activity implements this interface, and then enters the Activity

Activity.dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    
    
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    
    
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
    
    
            return true;
        }
        return onTouchEvent(ev);
    }

This method is used by Activity to handle touch screen events. We can rewrite this method and return true/false, so that the event can be intercepted before it is distributed to the window, and the ViewGroup or View in the Activity will not receive the event.

A touch screen event starts with ACTION_DOWN, so it will definitely enter the onUserInteraction() method

public void onUserInteraction() {
    
    
}

This is an empty method, and its calling timing is as follows: When a key event, touch screen event or trackball event is dispatched to the Activity, it will be called; if you want to know the user and device while the Activity is running This method can be rewritten; however, it should be noted that this method only responds to the touch gesture of touch-down, and will not respond to the subsequent touch-move and touch-up

A method corresponding to this method is onUserLeaveHint , which is also an empty method, and its calling timing is as follows:

When the Activity enters the background under user operation, this method will be called as part of the Activity life cycle; for example, the user presses the home button, the current Activity will enter the background, it will be called, and it is called before onPause ; But for example, if a call comes in and the Activity enters the background passively, this method will not be called

Next enter the second if statement

getWindow().superDispatchTouchEvent

The object obtained through getWindow() is a Window object, but it is instantiated in the attach method of the Activity. The actual type is PhoneWindow, and the Callback interface is also implemented here.

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
    
    
            ......
            mWindow = new PhoneWindow(this, window);
            mWindow.setCallback(this);
            ......
}

Go to PhoneWindow here, as follows

PhoneWindow.superDispatchTouchEvent

//这是窗口的顶层视图
private DecorView mDecor
@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
    
    
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView .superDispatchTouchEvent

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

DecorView is a subclass of FrameLayout, and FrameLayout is a subclass of ViewGroup, here we will come to ViewGroup

ViewGroup.dispatchTouchEvent

        @Override
    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;

            // 处理 initial down 发生后的初始化操作
            if (actionMasked == MotionEvent.ACTION_DOWN) {
    
    
                // 新的 ACTION_DOWN 事件来了,需要取消并清除之前的 touch Targets
                //清空掉 mFirstTouchTarget
                cancelAndClearTouchTargets(ev);
                //重置触摸状态
                resetTouchState();
            }

            //标记是否拦截事件
            final boolean intercepted;

            // 当 ACTION_DOWN 来了或者已经发生过 ACTION_DOWN,并且将 mFirstTouchTarget 赋值 就检测 ViewGroup 是否需要拦截事件.
            //只有发生过 ACTION_DOWN 事件,mFirstTouchTarget != null
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
    
    

                //子 View 可以通过调用父 View 的 requestDisallowInterceptTouchEvent 方法设置 mGroupFlags 值
                //以此告诉父 View 是否拦截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //如果子 view 没有告诉父 View 别拦截事件,那父 View 就判断自己是否需要拦截事件
                if (!disallowIntercept) {
    
    
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // 重新恢复 action  以防被改变了
                } else {
    
    
                        //这里表明子 View 告诉父 View 不要拦截事件
                    intercepted = false;
                }
            } else {
    
    
                //当 mFirstTouchTarget=null(没有子 View 被分配处理),且不是 initial down 事件时(事件已经初始化过了),ViewGroup 继续拦截触摸
                //继续设置为 true
                intercepted = true;
            }



            // 如果当前事件是 ACTION_CANCEL,或者 view.mPrivateFlags 被设置了 PFLAG_CANCEL_NEXT_UP_EVENT
            //那么当前事件就取消了
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            //split 表示当前的 ViewGroup 是不是支持分割 MotionEvent 到不同的 View 当中
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            //新的 TouchTarget
            TouchTarget newTouchTarget = null;
            //是否把事件分发给了新的 TouchTarget
            boolean alreadyDispatchedToNewTouchTarget = false;
            //不取消事件,同时不拦截事件才进入该区域
            if (!canceled && !intercepted) {
    
    

                //把事件分发给所有的子视图,寻找可以获取焦点的视图
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                //如果是这三种事件就得遍历子 View
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    
    

                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // 对于这个 PointerId 清空更早的 touch targets 
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    //如果当前 ViewGroup 有子 View 且 newTouchTarget=null
                    if (newTouchTarget == null && childrenCount != 0) {
    
    
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);

                        // 在视图里从前到后扫描一遍获取可以接收事件的子 View
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;

                        //遍历所有子 View,找到一个来接收事件
                        for (int i = childrenCount - 1; i >= 0; i--) {
    
    
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            //如果当前子 View 没有获取焦点,则跳过这个子 View
                            if (childWithAccessibilityFocus != null) {
    
    
                                if (childWithAccessibilityFocus != child) {
    
    
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            //如果当前子 View 不可见且没有播放动画 或者 不在触摸点范围内,跳过这个子 View
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
    
    
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            //如果在触摸目标列表找到了与该子 View 对应的 TouchTarget,说明这个 view 正在接收事件,不需要再遍历,直接退出
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
    
    
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }


                            resetCancelNextUpFlag(child);

                            //子 view 处于触摸位置,就将事件分发给子 View,如果该子 View 返回 true,说明消费了这个事件,就跳出遍历
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    
    
                                // 获取 TouchDown 的时间点
                                mLastTouchDownTime = ev.getDownTime();
                                // 获取 TouchDown 的 Index
                                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;
                                }
                                //获取 TouchDown 的 x,y 坐标
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //添加到触摸目标列表 同时给 mFirstTouchTarget 赋值
                                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();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
    
    
                        // 到这里说明没有子 View 接收事件,那就把最近一次的触摸目标赋值给 newTouchTarget
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
    
    
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // mFirstTouchTarget 赋值是在通过 addTouchTarget 方法获取的;
            // 只有处理 ACTION_DOWN 事件,才会进入 addTouchTarget 方法。
            // 这也正是当 View 没有消费 ACTION_DOWN 事件,则不会接收其他 MOVE,UP 等事件的原因
            if (mFirstTouchTarget == null) {
    
    
                // 那就只能 ViewGroup 自己处理事件了
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
    
    
                // 到这里就说明有子 View 接收了 ACTION_DOWN 事件,那后续的 move up 等事件就继续分发给这个触摸目标
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
    
    

                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    
    
                        handled = true;
                    } else {
    
    
                        //如果 view.mPrivateFlags 被设置了 PFLAG_CANCEL_NEXT_UP_EVENT 或者事件被 ViewGroup 拦截了
                        //那子 View 需要取消事件
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;

                        //继续分发事件给子 View
                        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;
                }
            }


            //当发生抬起或取消事件,更新触摸目标列表
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    
    
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
    
    
                //如果是多点触摸下的手指抬起事件,就要根据 idBit 从 TouchTarget 中移除掉对应的 Pointer(触摸点)
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

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

This method has a lot of content and needs to be split and analyzed

Step 1: Event initialization

The first to come in is the ACTION_DOWN event, which requires some initialization:

  • The first thing is to clear all TouchTargets and set the value of mFirstTouchTarget to null; the type of mFirstTouchTarget is also TouchTarget, which is an internal class of ViewGroup, which describes a touched view and the id of the pointer it captures; mFirstTouchTarget can be understood as if the event is triggered by a child When View is processing, mFirstTouchTarget will be assigned and point to the child View
  • The second thing is to reset the state value, reset the mGroupFlags value through FLAG DISALLOW INTERCEPT
ViewGroup.cancelAndClearTouchTargets
/**
      * 取消和清空所有的 touch targets.
      */
    private void cancelAndClearTouchTargets(MotionEvent event) {
    
    
        if (mFirstTouchTarget != null) {
    
    
            boolean syntheticEvent = false;
            if (event == null) {
    
    
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
    
    
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
    
    
                event.recycle();
            }
        }
    }

    /**
     * 清空所有的 touch targets.
     */
    private void clearTouchTargets() {
    
    
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
    
    
            do {
    
    
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }

    /**
     * 重置所有触摸状态以准备新周期.
     */
    private void resetTouchState() {
    
    
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

The second step: intercept judgment

Next, you need to judge whether you need to intercept the event:

First look at the conditions

            //标记是否拦截事件
            final boolean intercepted;

            // 当 ACTION_DOWN 来了或者已经发生过 ACTION_DOWN,并且将 mFirstTouchTarget 赋值 就检测 ViewGroup 是否需要拦截事件.
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
    
    

                //子 View 可以通过调用父 View 的 requestDisallowInterceptTouchEvent 方法设置 mGroupFlags 值
                //以此告诉父 View 是否拦截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //如果子 view 没有告诉父 View 别拦截事件,那父 View 就判断自己是否需要拦截事件
                if (!disallowIntercept) {
    
    
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // 重新恢复 action  以防被改变了
                } else {
    
    
                    //这里表明子 View 告诉父 View 不要拦截事件
                    intercepted = false;
                }
            } else {
    
    
                //当 mFirstTouchTarget=null(没有子 View 被分配处理),且不是 initial down 事件时(事件已经初始化过了),ViewGroup 继续拦截触摸
                //继续设置为 true
                intercepted = true;
            }
  • When the event is ACTION DOWN or mFirstTouchTarget != null, it will judge whether to intercept or not. From the first step, we can see that when the event is ACTION DOWN, mFirstTouchTarget must be null, so there are only two cases here: ACTION DOWN event comes It is necessary to judge the interception; if there is a sub-View in the ACTION DOWN event that receives the event (so mFirstTouchTarget is assigned), then the next event also needs to judge whether to intercept the event
  • The reverse logic of the above condition is that the event is an event after the ACTION DOWN event (such as move or up) and mFirstTouchTarget is null, which means that in the ACTION DOWN event, it is judged that the event needs to be intercepted or there is no child View to handle the event, then the next event There is no need to distribute, continue to intercept

In the first if statement, the interception judgment logic is

  • First obtain the value of mGroupFlags through the AND operation, and the child view can set the value of mGroupFlags by calling the requestDisallowInterceptTouchEvent method of the parent view to tell the parent view not to intercept the event
  • If disallowIntercept is true, it means that the child view requires the parent view not to intercept, so set intercepted to false
  • If disallowIntercept is false, it means that the child view has not requested not to intercept the request, then call onInterceptTouchEvent to see if you need to intercept the event
ViewGroup.requestDisallowInterceptTouchEvent
@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    
    

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
    
    
            // 如果已经设置过了,就返回
            return;
        }

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

        // 依次告诉父 view
        if (mParent != null) {
    
    
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }
ViewGroup.onInterceptTouchEvent
 /**
     * ViewGroup 可在这个方法里拦截所有触摸事件,默认是不拦截事件,开发者可以重写这个方法决定是否要拦截
     * 如下四个条件都成立,返回 true,拦截事件
     * 第一个:触摸事件是否来自鼠标指针设备
     * 第二个:触摸事件是否是 ACTION_DOWN
     * 第三个:检查是否按下了鼠标或手写笔按钮(或按钮组合),也就是说用户必须实际按下
     * 第四个:触摸点是否在滚动条上
     */
    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;
    }

Step 3: ACTION_DOWN event distribution

Next, you need to traverse the sub-Views, and then distribute the ACTION_DOWN event to the sub-Views that can receive events

  • If the current sub-View does not get the focus, skip this sub-View
  • If the current sub-View is invisible and does not play animation or is not within the touch point range, skip this sub-View
  • If the TouchTarget corresponding to the child View is found in the touch target list, it means that the view is receiving events, and there is no need to traverse again, just exit
  • If the child view is at the touch position, call the dispatchTransformedTouchEvent method to distribute the event to the child View. If the method returns true, it means that the child View has consumed the event, so there is no need to find the child view to receive the event, and jump out of the traversal
ViewGroup.dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    
    
    final boolean handled;

    // 发生取消操作时,不再执行后续的任何操作
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    
    
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
    
    
            handled = super.dispatchTouchEvent(event);
        } else {
    
    
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    //由于某些原因,发生不一致的操作,那么将抛弃该事件
    if (newPointerIdBits == 0) {
    
    
        return false;
    }

    //分发的主要区域
    final MotionEvent transformedEvent;
    //判断预期的 pointer id 与事件的 pointer id 是否相等
    if (newPointerIdBits == oldPointerIdBits) {
    
    
        if (child == null || child.hasIdentityMatrix()) {
    
    
            if (child == null) {
    
    
                //不存在子视图时,ViewGroup 调用 View.dispatchTouchEvent 分发事件,再调用 ViewGroup.onTouchEvent 来处理事件
                handled = super.dispatchTouchEvent(event); 
            } else {
    
    
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
                //将触摸事件分发给子 ViewGroup 或 View;
                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY); //调整该事件的位置
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event); //拷贝该事件,来创建一个新的 MotionEvent
    } else {
    
    
        //分离事件,获取包含 newPointerIdBits 的 MotionEvent
        transformedEvent = event.split(newPointerIdBits);
    }

    if (child == null) {
    
    
        //不存在子视图时,ViewGroup 调用 View.dispatchTouchEvent 分发事件,再调用 ViewGroup.onTouchEvent 来处理事件
        handled = super.dispatchTouchEvent(transformedEvent); 
    } else {
    
    
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
    
    
            //将该视图的矩阵进行转换
            transformedEvent.transform(child.getInverseMatrix());
        }
        //将触摸事件分发给子 ViewGroup 或 View;
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    //回收 transformedEvent
    transformedEvent.recycle();
    return handled;
}

This method is where ViewGroup actually handles events, distributes sub-Views to consume events, and filters out irrelevant pointer ids. When the child view is null, the MotionEvent will be sent to the ViewGroup; if it is not null, the View.dispatchTouchEvent method will be called to distribute the event.

After this method is called, returning to ViewGroup.dispatchTouchEvent will call the addTouchTarget method

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

You can see that mFirstTouchTarget is assigned a value here

When the child control consumes events, mFirstTouchTarget is not empty; when the child control does not consume events or is intercepted, mFirstTouchTarget is empty

Step 4: ACTION MOVE ACTION UP event distribution

After the third step, ViewGroup may find sub-View consumption events

  • If the event is intercepted, mFirstTouchTarget==null, then the next event will finally call the View.dispatchTouchEvent method to distribute the event
  • If ViewGroup has no child View, mFirstTouchTarget==null, then the same as above
  • If there is a child View, but the child View does not consume events, mFirstTouchTarget==null, then the same as above
  • If there is a child View, and the child View consumes the ACTION_DOWN event, but returns false in dispatchTouchEvent (that is, dispatchTransformedTouchEvent returns false, then addTouchTarget will not be called), mFirstTouchTarget==null, then the next processing is the same as above
  • The next thing is that mFirstTouchTarget is not null, then you need to distribute the subsequent events to the View that consumes the ACTION_DOWN event

Through the analysis of the ViewGroup.dispatchTouchEvent method, we know that no matter whether there is a child View consumption event, the final event will enter the View.dispatchTouchEvent method, so let's continue to find out

View.dispatchTouchEvent
/**
     * 将触摸事件向下传递到目标视图,或者这个 View 是目标视图。
     *
     * @return 返回 true 表示消费了事件,反之返回 false 
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
    
    

                ......

        boolean result = false;

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
    
    
            //在 Down 事件之前,如果存在滚动操作则停止。不存在则不进行操作
            stopNestedScroll();
        }

        //过滤触摸事件以应用安全策略
        if (onFilterTouchEventForSecurity(event)) {
    
    

            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
    
    
                result = true;
            }

            ListenerInfo li = mListenerInfo;
            // 如果给 View 设置了 OnTouchListener
            //且该 view 没有禁用的
            //且 OnTouchListener.onTouch 返回 true
            //那说明该 View 消费了该事件,返回 true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
    
    
                result = true;
            }

           //如果 OnTouchListener.onTouch 没有消费事件且 View 的 onTouchEvent 方法返回 true,那返回 true
            if (!result && onTouchEvent(event)) {
    
    
                result = true;
            }
        }

        // 如果这是手势的结束,则在嵌套滚动后清理;
        //如果我们尝试了 ACTION_DOWN 但是我们不想要其余的手势,也要取消它。
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
    
    
            stopNestedScroll();
        }

        return result;
    }

There are two important points here

  • If the developer sets OnTouchListener to monitor and returns true in the onTouch method, it means that the view consumes the event
  • If no listener is set, then call the View's onTouchEvent method to handle the event

It can be seen that OnTouchListener.onTouch is executed prior to onTouchEvent, as long as the former returns true, the latter will not be executed, and the event ends here

Next, look at the logic of onTouchEvent

View.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();

        //如果这个 view 是禁用的,可以通过 setEnabled()设置是否禁用
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
    
    
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    
    
                setPressed(false);
            }
            // 即使设置了禁用,但是只要这个 view 满足 CLICKABLE ,LONG_CLICKABLE ,CONTEXT_CLICKABLE 其中一种
            //任然算消费该事件,只是没有响应而已
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

         //当 View 状态为 ENABLED
        //且这个 view 满足 CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE 其中一种,就消费这个事件
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    
    
            switch (action) {
    
    
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    
    
                        // 获取焦点处于可触摸模式
                        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) {
    
    
                            //这是 Tap 操作,移除长按回调方法
                            removeLongPressCallback();

                            // 如果处于按下状态尽执行点击操作
                            if (!focusTaken) {
    
    
                                // 使用 Runnable 并发布而不是直接调用 performClick 
                                //这样可以在单击操作开始之前更新视图的其他可视状态
                                if (mPerformClick == null) {
    
    
                                    mPerformClick = new PerformClick();
                                }
                                //调用 View.OnClickListener
                                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:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
    
    
                        break;
                    }

                    // 确定是否处于可滚动的视图内
                    boolean isInScrollingContainer = isInScrollingContainer();

                    if (isInScrollingContainer) {
    
    
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
    
    
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        //当处于可滚动视图内,则延迟 TAP_TIMEOUT,再反馈按压状态,用来判断用户是否想要滚动。默认延时为 100ms
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

                    } else {
    
    
                        //当不再滚动视图内,则立刻反馈按压状态
                        setPressed(true, x, y);
                        //检测是否是长按,如果长按,回调 OnLongClickListener.onLongClick
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
    
    
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
    
    
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

Here are a few things to note

  1. As long as the view satisfies one of CLICKABLE, LONG CLICKABLE, and CONTEXT CLICKABLE, regardless of whether it is disabled or enabled through setEnabled(), it will return true and consider the consumption event
  2. The longClickable of View is false by default, and the clickable needs to be distinguished. For example, the clickable of Button is true by default, and the clickable of TextView is false by default; but the setOnClickListener of View will set the clickable of View to true by default, and the setOnLongClickListener of View will also set the clickable of View to longClickable is set to true
  3. In the ACTION_DOWN operation, if it is a long press, callback OnLongClickListener.onLongClick
  4. In the ACTION_UP operation, callback OnClickListener.onClick
Activity.OnTouchEvent

After all the processes are completed, if there is no View consumption event, it will eventually return to Activity.OnTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    
    
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    
    
            onUserInteraction();
        }
        //循环判断是否有 ViewGroup 或者 View 消费事件,如果没有,事件回到 activity
        if (getWindow().superDispatchTouchEvent(ev)) {
    
    
            return true;
        }
        return onTouchEvent(ev);
    }

public boolean onTouchEvent(MotionEvent event) {
    
    
        if (mWindow.shouldCloseOnTouch(this, event)) {
    
    
            finish();
            return true;
        }

        return false;
    }

Event Distribution Flowchart

image.png

important point

  1. The touch event is first processed by Activity.dispatchTouchEvent, and then distributed layer by layer. When the ViewGroup in the middle does not consume or intercept, it enters the bottom View and starts to be processed by the bottom OnTouchEvent. If it has not been consumed, the last Return to Activity.OnTouchEvent
  2. Only ViewGroup has the onInterceptTouchEvent interception method; during the distribution process, any layer of ViewGroup in the middle can be directly intercepted, and it will no longer be distributed downwards, but will be handled by the OnTouchEvent of the ViewGroup where the interception operation occurs
  3. The child View can call the requestDisallowInterceptTouchEvent method of the parent ViewGroup to set disallowIntercept=true, thereby preventing the onInterceptTouchEvent interception operation of the parent ViewGroup
  4. When OnTouchEvent bubbles from bottom to top, when any layer of OnTouchEvent in the middle consumes the event, it will no longer be passed up, indicating that the event has been consumed
  5. If dispatchTouchEvent is dispatching events, View does not consume ACTION DOWN events, that is, returns true, then subsequent events such as ACTION MOVE will not be received
  6. Regardless of whether the View is DISABLED (disabled) or ENABLED (available), as long as it is CLICKABLE (clickable), LONG_CLICKABLE (long pressable), events will be consumed
  7. View's setOnClickListener will set View's clickable to true by default, and View's setOnLongClickListener will also set View's longClickable to true; all View's setClickable and setLongClickable are best called after the two listening methods
  8. onTouch is executed prior to onTouchEvent, onClick and onLongClick are called in onTouchEvent, and onLongClick is executed prior to onClick; if onTouch returns true, onTouchEvent will not be executed; onTouch only executes this method if the View has set OnTouchListener and is enabled

Guess you like

Origin blog.csdn.net/m0_70748845/article/details/130047032