This time, the event distribution mechanism in Android will generate a chain of events from the start of the Activity to the decorView to the innermost view from the moment the screen is clicked. Each layer view or Viewgroup first calls its dispatchTouchEvent method and then decides whether to consume events at the current layer

View event distribution

First, a piece of pseudo-code, which I saw in a book, is the best summary I think

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean isConsume = false;
    if (isViewGroup) {
        if (onInterceptTouchEvent(event)) {
            isConsume = onTouchEvent(event);
        } else{ isConsume = child.dispatchTouchEvent(event); }}else {
        //isView
        isConsume = onTouchEvent(event);
    }
    return isConsume;
}
Copy the code

If it is currently in the ViewGroup hierarchy, onInterceptTouchEvent is checked to see if it is true. If it is true, the event will be consumed in this hierarchy and will not be passed down. The onTouchEvent method of the current ViewGroup is then executed. If onInterceptTouchEvent is false, the event is passed to the dispatchTouchEvent method at the next level, followed by the same code logic, all the way to the innermost view.

Ok, that’s not all. When we get to the innermost layer we’re going to execute onTouchEvent directly, does the view have the right to reject the consumption event? View, as the lowest level, should have no right to speak. However, as a matter of fairness, the View can also be rejected by returning false in the onTouchEvent method to indicate that it does not want to consume the event. So how will this event be handled? See the pseudocode below:

public void handleTouchEvent(MotionEvent event) {
    if (!onTouchEvent(event)) {
        getParent.onTouchEvent(event);
    }
}
Copy the code

If the view’s onTouchEvent method returns false, its parent’s onTouchEvent will be called. If the parent’s onTouchEvent returns false, the parent’s onTouchEvent will be called. All the way up to the top level, the onTouchEvent for the Activity is called.

So now the consumption process is done but what about onTouch, onTouchEvent and onClick? Here’s another pseudocode:

public void consumeEvent(MotionEvent event) {
    if (setOnTouchListener) {
        onTouch();
        if (!onTouch()) {
            onTouchEvent(event);
        }
    } else {
        onTouchEvent(event);
    }

    if(setOnClickListener) { onClick(); }}Copy the code

When onInterceptTouchEvent of a layer viewGroup is called, it means that the current layer consumes events. If its onTouchListener is set, onTouch will be called. If onTouch returns true, onTouchEvent will not be called. If false is returned or onTouchListener is not set, onTouchEvent continues to be called. The onClick method is set to onClickListener and will be called normally.

Here is a flowchart to summarize:

Source code analysis

A touch event goes first to the Activity level, then to the root view, through the layers of view groups and finally to the innermost view, and we parse it layer by layer

The Activity (dispatchTouchEvent)

Go straight to code

	//Activity.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
     public void onUserInteraction(a) {}Copy the code

As you can see, the onUserInteraction method is empty, mainly because the getWindow().superDispatchTouchEvent(EV) method is called, which returns true, indicating that the event is consumed. Return false to indicate that there is no processing at the lower level, which goes directly to the activity’s onTouchEvent method, which is also consistent with the previous consumption pass.

Continue with the superDispatchTouchEvent method and then go to the PhoneWindow superDispatchTouchEvent method and the DecorView superDispatchTouchEvent method. Look at the code:

    //PhoneWindow.java
    private DecorView mDecor;
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    
    //DecorView.java
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

Copy the code

As you can see, the PhoneWindow is passed through to the DecorView, which is the root view of the activity and the parent view of the setcontentView, inherited from FrameLayout. So the super. DispatchTouchEvent (event) method here, is actually going to the dispatchTouchEvent method of the viewGroup.

ViewGroup (dispatchTouchEvent)

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (onFilterTouchEventForSecurity(ev)) {
            // Check for interception
            final boolean intercepted;
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
              / / FLAG_DISALLOW_INTERCEPT flag is through requestDisallowInterceptTouchEvent set
                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 are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
          
          
          / / mFirstTouchTarget assignment
             while(target ! =null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    } else {
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            continue; }}}}Copy the code

Some of the key code is captured here, starting with two conditions

  • actionMasked == MotionEvent.ACTION_DOWN

  • mFirstTouchTarget ! = null

If one of these conditions is met, the onInterceptTouchEvent method will be executed. Otherwise, intercepted = true will be used. The first condition is obviously ACTION_DOWN. The second condition is a field. As you can see from the following code, the mFirstTouchTarget field is assigned when a view consumes the event, otherwise it is null.

So what does that mean? When the ACTION_DOWN event happens, it’s going to execute the following code. If the current viewGroup has already consumed the event and hasn’t passed it to the child view, then the mFirstTouchTarget field will be empty, so it won’t execute the following code and will consume all the events. This fits into the mechanism mentioned earlier:

Once a view intercepts, all subsequent events are handled by it and the onInterceptTouchEvent method is not executed

However, if one of the two conditions is met, will onInterceptTouchEvent be implemented? Not necessarily. Here we see another criterion: disallowIntercept. Behind this field is set by requestDisallowInterceptTouchEvent method, we’ll talk about, it is mainly used for sliding conflicts, which means the child view tell you don’t want you to intercept, then you have not stopped, direct return false.

Ok, looking at the source code, we saw earlier that if the viewGroup doesn’t intercept the event, it should pass it to the child view, so where does it pass it? Continue with the code for dispatchTouchEvent:

if(! canceled && ! intercepted) {final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null&& childrenCount ! =0) {
                        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(childWithAccessibilityFocus ! =null) {
                                if(childWithAccessibilityFocus ! = child) {continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            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);
                        }
                        if(preorderedList ! =null) preorderedList.clear(); }}}Copy the code

So you can see here that we’re going through a child view, where if one of the two conditions is met, it jumps out. Otherwise, perform dispatchTransformedTouchEvent method. Let’s look at these two conditions:

  • ! child.canReceivePointerEvents()
  • ! isTransformedTouchPointInView(x, y, child, null)

The name doesn’t tell you anything, but the code:

    protected boolean canReceivePointerEvents(a) {
        return(mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() ! =null;
    }
    
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if(isInView && outLocalPoint ! =null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }
Copy the code

Oh, that’s what IT means. The canReceivePointerEvents method indicates whether the view can accept click events, such as animations. And isTransformedTouchPointInView method is representative of the coordinates of the click event on the view area. Ok, if the conditions are met, is executed to the dispatchTransformedTouchEvent method:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case. We don't need to perform any transformations
        // or filtering. The important part is the action, not the contents.
        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);
            returnhandled; }}Copy the code

This method, as you might have guessed, is actually called Child.dispatchTouchEvent (Event). So that’s the dispatchTouchEvent method of the view at the next level, which starts the hierarchical transmission of events.

The View (dispatchTouchEvent)

When we get to the View level, we’re going to execute the view’s dispatchTouchEvent code

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

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

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if(li ! =null&& li.mOnTouchListener ! =null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if(! result && onTouchEvent(event)) { result =true; }}if(! result && mInputEventConsistencyVerifier ! =null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if(actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && ! result)) { stopNestedScroll(); }return result;
    }
Copy the code

As you can see, li.monTouchListener! = null, if not null, the onTouch method is executed. Based on the result returned by the onTouch method, if false, result will be false and the onTouchEvent will be executed. This logic is also consistent with the delivery mode we mentioned before.

Finally, let’s look at what the View’s onTouchEvent does:

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! =0;
                    if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if(isFocusable() && isFocusableInTouchMode() && ! isFocused()) { focusTaken = requestFocus(); }if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) {// This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if(! focusTaken) {// Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if(! post(mPerformClick)) { performClickInternal(); } } } } mIgnoreNextUpEvent =false;
                    break;
            }

            return true;
        }
Copy the code

As you can see from the code, if CLICKABLE or LONG_CLICKABLE is set, the view will consume the event, execute the performClickInternal method, and then execute to the performClick method. So this performClick method, which you’re probably all familiar with, is the method that triggers the click, but inside it is the onClick method.

    private boolean performClickInternal(a) {
        notifyAutofillManagerOnClick();
        return performClick();
    }
    
    public boolean performClick(a) {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if(li ! =null&& li.mOnClickListener ! =null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        return result;
    }

Copy the code

So far, the source code also see about, in fact, there are a lot of details, here is not to say, you can go to study.

The application of event distribution (requestDisallowInterceptTouchEvent)

Now that we’ve learned the event distribution mechanism, how do we actually apply it to our work? In fact, the most common is to solve the problem of sliding conflicts. There are generally two solutions:

  • One is external interception, which is handled from the parent view side and determines whether the event is distributed to the child view
  • One is internal interception: the child view side processing, according to the situation to decide whether to prevent the parent view to intercept, the key isrequestDisallowInterceptTouchEventMethods.

The first method is to check whether the onInterceptTouchEvnet method returns true or false. The second approach, is to use the requestDisallowInterceptTouchEvent method, the meaning of this method is to make the parent view don’t go to intercept events, in the sign bit in dispatchTouchEvent method: FLAG_DISALLOW_INTERCEPT: If the disallowIntercept field is true, the onInterceptTouchEvent method is not executed. Instead, it returns false and does not intercept the event.

The code:

    // External interception: parent view.java
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        // The parent view intercepts the condition
        boolean parentCanIntercept;

        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (parentCanIntercept) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        return intercepted;

    }
Copy the code

External interception is simply a matter of judging the conditions and deciding whether or not to intercept.

    / / the parent view. Java
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true; }}/ / child view. Java
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        // The parent view intercepts the condition
        boolean parentCanIntercept;

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (parentCanIntercept) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(event);
    }
Copy the code

Internal interception is a bit complicated. We need to rewrite the parent view method.

  • When the parent view is ACTION_DOWN, it should not be blocked, because if it is blocked, subsequent events have nothing to do with the child view
  • The parent view should return true for other events, indicating interception. Because the onInterceptTouchEvent method is called by the FLAG_DISALLOW_INTERCEPT bit, the onInterceptTouchEvent method is called by the onInterceptTouchEvent method only when the parent view intercepts it. So you want to make sure that you’re intercepting something in the method.

At this point, the distribution mechanism for events is enough. Please correct me if I am wrong, thank you.


One of your 👍 is my motivation to share ❤️.