Android source code analysis – event distribution mechanism

It’s a great way to learn something by asking questions. Learning the event system of View in Android, I also ask questions for myself and know the principle while solving the problem.

0

Let’s start with a few questions:

  • What is an event?
  • What is the event distribution mechanism?

Every tap, long press, move, etc. is an event when we interact with the phone through the screen. In object-oriented thinking, these events are encapsulated as motionEvents. The distribution mechanism is that an event is passed from the screen to the various views in the app View, and then one of the views either uses the event or ignores the event, and the whole process is controlled by the distribution mechanism. Note that in event distribution, events are distributed to the View as a sequence of events. The sequence starts with ACTION_DOWN, goes through a series of ACTION_MOVE events, and ends with an ACTION_UP event. All events in this sequence are either ignored or only one event can be used. If the same sequence of actions, such as pressing and moving, were accepted by different views, the whole interface would be very confusing and logically complex. Here are three questions:

  • What is the general flow of an event going from the screen all the way to the View?
  • I said that events are actually distributed as sequences of events. So what is the mechanism by which so many events in the same sequence are delivered to only one View?
  • Where is the OnClick OnLongClick listener that we set externally to the View during application development handled by the View?

Question 1: What is the process of event transmission?

Android View is a tree structure, as shown below:

Each Activity contains a Window inside to manage the view to display. Window is an abstract class whose concrete implementation is the PhoneWindow class. DecovrView, as an inner class of PhoneWindow, actually manages the display of the concrete view. It is a subclass of FrameLayout, which holds our title bar and root view. He manages a bunch of views and viewgroups that we write ourselves. So when events are distributed, the big views at the top don’t actually do anything with them, they just keep sending them down until we can use them.

So, the top-down process of events should look like this:

Activity (not processed) -> root View -> layer by layer ViewGroup (if any) -> child View

What if at the end of the pass our child views didn’t handle the event? This will be returned to the original path and eventually passed to the Activity. This event is discarded only if the Activity does not process it either.

Activity (discard if not processed) <- root View <- layer by layer ViewGroup (if any) <- child View

When events are passed, they are controlled by the following three methods:

  • DispatchTouchEvent: dispatches events
  • OnInterceptTouchEvent: Intercepts the event
  • OnTouchEvent: Consumption event

The three methods have one thing in common: whether or not they perform their functions (distribute, intercept, consume) is determined by their return values. Returning true indicates that they have completed their functions (distribute, intercept, consume). The difference is not just functionality, but usage scenarios. Both the dispatchTouchEvent() and onTouchEvent() methods, whether Activity ViewGroup or View, are used. The onInterceptTouchEvent() method is only for intercepting events, so the Activity and View are at the top level and the View is at the bottom level. Therefore, there is no onInterceptTouchEvent() method in views and activities.

I’m going to customize viewGroups and views, rewrite their methods, and log them. Take a look at the results without adding any listeners (i.e., no View consumption events) :

Click on the external ViewGroup:



Click a View:

The ViewGroup dispatchTouchEvent() method is called onInterceptTouchEvent(). If the onInterceptTouchEvent() method is false, the ViewGroup dispatchTouchEvent() method is called. The child View calls its dispatchTouchEvent() method for distribution. Since the View has no onInterceptTouchEvent() method, there is no intercepting operation, so it passes the event directly to its onTouchEvent() method for consumption. Since my child View did not use this event, the onTouchEvent() method simply returns false to indicate that it did not consume, so the event is now passed to the end. Since it didn’t consume and therefore didn’t distribute, the child View’s dispatchTouchEvent() method returns false, returning the event to the upper-level ViewGroup. ViewGroup finds that this event has no child View consumption, so do it yourself! Pass the event to its own onTouchEvent() method for consumption. But the ViewGroup is not consuming either, so the onTouchEvent() method will have to return false. Similarly, the ViewGroup does not consume events itself, so its dispatchTouchEvent() method also returns false. If this is a bit confusing, post a graph to illustrate :(red arrows indicate the process of events being distributed from the top down, yellow shows the process of events being returned from the bottom up)

Next, I add the OnClick listener to the child View and see what happens when I click the child View:



At first glance, gee, why did I print log twice? It’s not that there’s anything wrong with it. I said that event distribution is a sequence of events, and I add click events, so I’m going to consume click events. The click event is actually divided into two events, namely ACTION_DOWN + ACTION_UP, which is the only one click event. So printing the log “twice” actually prints the ACTION_DOWN distribution process first and then the ACTION_UP distribution process again, so you’ll see the click event printed on the last line. That is, the click event occurs after the ACTION_UP event.

Then look at the return value of each method. Sure enough, since my child View explicitly wants to consume this sequence of events, it will consume all events starting with ACTION_DOWN. So the child View’s onTouchEvent returns true, which means it needs to consume the event, and its dispatchTouchEvent returns true, which means it sent the event. Since its child View consumes the event, the ViewGroup assumes that the event was distributed by itself, so its dispatchTouchEvent returns true. Here’s a picture to make it clear:

Finally, I force the return value of the onInterceptTouchEvent() method of the ViewGroup to true, which means that the event is intercepted when it reaches this layer.

Sure enough, although I want to consume the event in the child View, the event is intercepted by the ViewGroup before it reaches the child View, so the event will be consumed only by the ViewGroup, so the ViewGroup passes the event to its onTouchEvent(). Here’s another picture:

In summary, this is the general process of event distribution.

Question 2: How to ensure that the unified sequence of events are assigned to a View

If a View consumes the first event (ACTION_DOWN), the ViewGroup will store the View, and all other events in the same sequence will be handled by the View. Specific how to operate, need to look at the source code:

ViewGroup dispatchTouchEvent();

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // Omit some of the previous extraneous code

        // Handled is the result that is returned, indicating that it has been handled
        boolean handled = false;

        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Determine if there is ACTION_DOWN, if so, a new sequence of events is coming
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Pay attention to these two methods. Here we will do something equivalent to "zero clearing"
                // There is an initialization operation like mFirstTouchTarget=null
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // intercepted is used to record whether the result is intercepted or not
            final boolean intercepted;
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
                final booleandisallowIntercept = (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 is no mFirstTouchTarget and the event is non-ACTION_DOWN, so it is intercepted here
                intercepted = true;
            }

            // Ignore some interception-related code

            // Remember these two objects, we will encounter them later
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if(! canceled && ! intercepted) {If ACTION_DOWN is used, then a new sequence of events is started
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                      // Start traversing your child views
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null&& childrenCount ! =0) {
                        // Get the coordinates of the click, which can be used to filter the View clicked from the sub-view
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);

                        // Iterate through the child views from back to front
                        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);

                            // The filter only filters out unsuitable views
                            // Continue, one at a time, goes straight to the next loop if the View is found to be inappropriate
                            if(childWithAccessibilityFocus ! =null) {
                                if(childWithAccessibilityFocus ! = child) {continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child,null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            // The appropriate child View is found. Note that the child View is encapsulated as a target
                            // Exit the loop if the result is not empty
                            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;
                            }

                            / / even if return result is empty it doesn't matter, continue to recursive call child View here dispatchTransformedTouchEvent ()
                            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; }}if(preorderedList ! =null) preorderedList.clear();
                    }

                    // The View to accept the event was not found
                    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; }}}// Then there are two cases for non-ACTION_down events
            if (mFirstTouchTarget == null) {
                / / 1. There just isn't found to accept the view of events, or be intercepted, call its own dispatchTransformedTouchEvent () and wore a null view inside, so what's the use? Behind the need analysis dispatchTransformedTouchEvent ()
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                //2. If a View accepts ACTION_DOWN events, the View will also accept other events
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while(target ! =null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        / / alreadyDispatchedToNewTouchTarget this variable in the front View to accept ACTION_DOWN event set to true
                        // The mFirstTouchTarget is the target wrapped by the View
                        // Then the return value is handled as true
                        handled = true;
                    } else {
                        / / for non ACTION_DOWN event, is still a recursive call dispatchTransformedTouchEvent
                        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; }}// Handle ACTION_UP and ACTION_CANCEL
            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;
    }
Copy the code

Then look at dispatchTransformedTouchEvent () source code:


/ / the front on the analysis of the dispatchTouchEvent () found that there are many calls the dispatchTransformedTouchEvent (), and in some places of the third parameter is null
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

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

        // Ignore some code...

        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    // Call itself dispatchTouchEvent() if child is null
                    handled = super.dispatchTouchEvent(event);
                } else {
                  // Not null, then call his dispatchTouchEvent()
                    handled = child.dispatchTouchEvent(event);
                }
                returnhandled; }}else {
          / /...
        }
        if (child == null) {
            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());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

Copy the code

Above is the dispatchTouchEvent () and dispatchTransformedTouchEvent () analysis, looks a bit messy, comb here:

  • To be clear, event distribution starts with the ViewGroup’s dispatchTouchEvent()
  • When a ViewGroup encounters a new sequence of events, ACTION_DOWN, it starts iterating through all of its child views to find the View that needs to receive the event
  • Whether or not found, will be called dispatchTransformedTouchEvent () method, the difference is that if found, so in this method the incoming is the View, otherwise it is null
  • DispatchTransformedTouchEvent child () method of the third parameter is null, will call the superclass dispatchTouchEvent () method, otherwise will call the child dispatchTouchEvent () method. In short, the View class’s dispatchTouchEvent() method is called.
  • DispatchTransformedTouchEvent () method is a specific event distribution, in addition to the OnClick () event, onTouchEvent () method is invoked here
  • Once a View is found to receive an event, it is encapsulated as a target, saved, and all subsequent events are accepted by it

Question 3: OnClick OnLongClick and other external monitoring is handled in where?

First consider a very simple logic. The OnClick event is ACTION_DOWN first and ACTION_UP later, so it must be handled in onTouchEvent(). Similarly, OnLongClick occurs after ACTION_DOWN has been held for a while, so it is also handled in onTouchEvent(). Take a look at the source code, found sure enough here:

// The following source code is ignored, only the key points
public boolean onTouchEvent(MotionEvent event) {
    / /...
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                    if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) {/ / handle the click
                        if(! focusTaken) {if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if(! post(mPerformClick)) { performClick(); }}}}break;

            case MotionEvent.ACTION_DOWN:
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    / /...
                } else {
                    / / longclick processing
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                / /...
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_MOVE:
                / /...
                break;
        }

        return true;
    }

    return false;
}

Copy the code

According to the previous analysis, in the View’s dispatchTouchEvent() method, the


public boolean dispatchTouchEvent(MotionEvent event) {
    / /...
    boolean result = false;

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

    / /...

    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        // As long as the ListenerInfo obtained is not empty, we have set the listener, then we think that we want the View to handle all events
        ListenerInfo li = mListenerInfo;
        if(li ! =null&& li.mOnTouchListener ! =null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {// So onTouch() will be executed here
            result = true;
        }

        // If there is no processing, onTouchEvent() is called again until onTouchEvent() also returns false
        if(! result && onTouchEvent(event)) { result =true; }}return result;
}

Copy the code

As you can see, in the View’s dispatchTouchEvent() method, the event is determined to be consumed by checking whether a listener is set, etc. The onTouchEvent() method is always called, and both Click and Longclick are in there. Regardless of internal processing, if true is returned, the event is considered consumed.

This is the end of the analysis, as a small chicken, analysis process is inevitable some mistakes and omissions, welcome to tell me in the comments section