Source Code Analysis of Android Event Distribution Mechanism

Keywords: Windows Android

Source Code Analysis of Android Event Distribution Mechanism

Speaking of Android's event mechanism, I believe many people are familiar with it. Detailed understanding of the rules of event transmission in Android enables us to better understand the logic of event handling and deal with complex issues such as sliding conflicts. This paper mainly analyses the distribution rules of events at each View level, and the detailed process from receiving events from Activity to consuming an event successfully.

Activity Distribution Process

When a touch event occurs in an activity, the event is first passed to the activity, which calls the dispatchTouchEvent method of the activity, and then starts the event distribution process. Now we can see that if we want to intercept all events, we can do so by rewriting the dispatchTouchEvent method of Activity. Let's take a look at Activity's dispatchTouchEvent method:

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

First, if it is an ACTION_DOWN event, the onUserInteraction method is invoked, which is implemented as an empty method in Activity and can do something by overwriting the method when an event sequence arrives. The getWindow method returns the Windows object associated with the Activity method. Windows is an abstract class, and its concrete implementation is PhoneWindow (as can be seen in the attach method of the Activity class: mWindow = new PhoneWindow(this, window);). Therefore, we can know that Activity simply handed the event to the superDispatch TouchEvent method of Windows, and if Windows failed to handle the event successfully, it handed it to the onTouchEvent method of Activity itself. Then take a look at the Windows process.

Window s Distribution Process

Let's first look at the implementation of the superDispatch TouchEvent method in Phone Windows:

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

PhoneWindow's superDispatch TouchEvent method is simple, that is, to hand events directly to mDecor. MDecor is an instance of DecorView, the top-level View in Activity, which is a subclass of FrameLayout, and its superDispatchTouchEvent method directly calls the super.dispatchTouchEvent method. The event distribution of layout managers such as FrameLayout, Linear Layout, Relative Layout inherits from ViewGroup, so at this point, the event is handled by ViewGroup. Let's take a look at how ViewGroup distributes events.

Distribution process of ViewGroup

The dispatch TouchEvent method in ViewGroup is relatively long and has more than 200 rows, so the unrelated part of logic will be ignored and analyzed step by step according to the event distribution logic.

As we all know, in Android, an event sequence is processed in units of an event sequence. An event sequence begins with an ACTION_DOWN event and ends with an ACTION_UP event. There are several ACTION_MOVE events in the middle. Now let's look at the processing logic of ViewGroup when an event arrives:

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

First, if the arrival event is ACTION_DOWN, that is to say, when a new event sequence arrives, it will perform initialization operation to clear the touchTarget and its state of the last event sequence. If it is not a new sequence of events (that is, the ACTION_DOWN event is not coming), skip this operation and start event distribution:

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

In the process of distribution, the ViewGroup first decides whether to intercept the event (interception is no longer distributed to the lower View). ViewGroup's onInterceptTouchEvent returns false by default, so when actionMasked = MotionEvent. ACTION_DOWN or mFirstTouchTarget!= null, ViewGroup does not intercept the event, otherwise intercepted = true. Further, if a new sequence of events arrives, or the sequence of events has been processed by the sub-View (i.e. mFirstTouchTarget!= null), the ViewGroup does not intercept the event and distributes it to the lower level, otherwise the event is intercepted (because there is no sub-View to process the sequence of events, so no further distribution is required) and handled by the ViewGroup. Then look at the processing logic when ViewGroup does not intercept events:

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

Although the above code looks long, the logic is clear. It should be noted that there is an if judgment outside this loop:

    if ( actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE )

Regardless of mouse and multi-finger events, only ACTION_DOWN events will enter the above cycle for distribution. That is to say, because ACTION_DOWN events must enter the cycle and touch Targets have been cleared at this time, the getTouchTarget method must return null when entering the above cycle. Then the distribution process is as follows:

Traversing through all the sub-Views of the ViewGroup, and then judging whether the sub-View can receive the event (by judging whether the View is visible or playing animation and whether the event falls in the area of the View), if the condition is not satisfied, executing continue to continue the cycle. The dispatchTransformedTouchEvent method is then called to distribute the events in turn to the child View to find a new touchTarget. If a child View handles the event, add a new target to the touchTargets chain by addTouchTarget, assign the mFirstTouchTarget, and end the loop.

    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    }

Now let's look at the processing when mFirstTouchTarget = null. There are two cases. The first is that ViewGroup intercepts the event, that is, the event is not ACTION_DOWN, and mFirstTouchTarget = null, that is to say, the ACTION_DOWN event of the event sequence is not processed by the child View. Second: The event is an ACTION_DOWN event, and if the event is not successfully distributed to the child View, then mFirstTouchTarget equals null. At this point, the dispatchTransformedTouchEvent method is called to hand the event over to itself (note: the third parameter at this time is null for its own processing).

Part of the dispatchTransformedTouchEvent method is as follows:

    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        event.offsetLocation(offsetX, offsetY);

        handled = child.dispatchTouchEvent(event);

        event.offsetLocation(-offsetX, -offsetY);
    }
    return handled;

The above code shows the main logic of the dispatchTransformedTouchEvent method, that is, if the child (the third parameter of the method) is null, the dispatchTouchEvent method of the parent class (that is, the View class) is invoked, otherwise the dispatchTouchEvent method of the child is invoked to pass events down. If the child is a View Group, the above distribution process will be repeated, and if the child is only a View (the View here does not contain the View Group), it will be handled by the dispatch TouchEvent of the View class.

If the ACTION_DOWN event does not arrive and is not intercepted, then the event must be handled by a child View, which is stored in the TouchTarget list headed by mFirstTouchTarget:

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

At this point, the event distribution process of ViewGroup is over. If the sub-View is still ViewGroup, the above process is repeated by the sub-View. If the sub-View is not ViewGroup, then the event processing process of View (ViewGroup excluded here) is entered.

View Distribution Process

Since View does not contain child Views, you don't need to continue distributing, just handle the event yourself when you receive it. Therefore, there is no event interception logic in the View, nor does it contain the onInterceptTouchEvent method. When an event is distributed to the View, the View processes the event (but not necessarily successfully). The processing logic of the dispatchTouchEvent method of the View class is as follows:

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

First, View checks whether OnTouchListener is set, if OnTouchListener is set, its onTouch method is executed, and if OnTouchListener returns true, the processing ends. Otherwise, the onTouchEvent method of View is executed. As can be seen from above, OnTouchListener has higher priority than onTouchEvent, and if OnTouchListener returns true, the onTouchEvent method will not be executed.

The onTouchEvent method of the View class handles different events accordingly, such as the onClick of OnClickListener (if set) by performing Click when the ACTION_UP event is received. It should be noted that if a View is set Enable (false), then the OnTouchListener and onTouchEvent of the View will not be invoked. However, as long as the clickable= true or long clickable==true of the View, the View consumes the event, as can be seen from the following code in the onTouchEvent method at the head:

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

Rule summary

Through the above analysis, we can summarize the following laws:

  1. Once the ViewGroup intercepts the event, the entire sequence of events (if it can be passed on to it) will be intercepted.
  2. If a View does not handle ACTION_DOWN events, subsequent events of the event sequence will not be handled by it.
  3. If a View handles ACTION_DOWN events, but does not process subsequent events, then the subsequent events of the event sequence will eventually be handled by Activity.
  4. View's onTouchEvent consumes events by default, unless its clickable and longClickable are both false.

Posted by @sh on Wed, 03 Apr 2019 19:12:29 -0700