Android event distribution mechanism

Posted by philippo on Tue, 07 Dec 2021 19:19:44 +0100

Original link: https://juejin.im/post/5eb3e0...

This time, let's talk about the event distribution mechanism in Android
Starting from clicking the screen, a series of events will be generated from Activity to decorview to the innermost view. Each layer of view or viewgroup will first call its dispatchTouchEvent method, and then judge whether the event is consumed in the current layer

Event distribution for view

First of all, the last pseudo code was seen in the book and I think it is the best summary

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

If the copied code is currently at the viewgroup level, it will judge whether onInterceptTouchEvent is true. If true, it means that the event will be consumed at this level and will not be passed down. The onTouchEvent method of the current viewgroup is then executed. If onInterceptTouchEvent is false, it means that the event continues to be passed to the dispatchTouchEvent method of the next level, and then the same code logic continues to the view of the innermost level.
ok, it's not over yet. The onTouchEvent method will be directly executed at the innermost layer. At this time, does the view have the right to reject consumption events? According to the truth, view, as the bottom layer, should have no right to speak. However, based on the principle of fairness and justice, the view can also be rejected. You can return false in the onTouchEvent method to indicate that he does not want to consume this event. So how will this incident be handled? See the following pseudo code:

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

Copy the code. If the onTouchEvent method of view returns false, the onTouchEvent of its parent container will be called again. If the onTouchEvent of the parent container returns false, it will be handed over to the upper level. Until the top layer, that is, the onTouchEvent of the Activity, is called.

At this point, the consumption process is completed
However, what is the calling relationship between onTouch, onTouchEvent and onClick?
Then another piece of pseudo code:

 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 view is called, it represents the event to be consumed by the current level. If its onTouchListener is set, onTouch will be called. If the return value of onTouch returns true, onTouchEvent will not be called. If false is returned or onTouchListener is not set, onTouchEvent will continue to be called. The onClick method is set, and onClickListener will be called normally.

Here is a flow chart to summarize:

< figcaption style = "margin: 10px 0px 0px; padding: 0px; Outline: 0px; max width: 100%; box sizing: border box! Important; overflow Wrap: break word! Important; line height: inherit; text align: Center; color: rgb (153, 153, 153); font size: 0.7em;" > insert picture description here < / figcaption >

Source code analysis

A touch event is first transferred to the Activity level, then to the root view, through layers of viewgroup s, and finally to the innermost view. We analyze it layer by layer
Activity(dispatchTouchEvent)
Direct 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() {
    }

Copy the code here. You can see that the onUserInteraction method is empty. It mainly calls the getWindow().superDispatchTouchEvent(ev) method. If it returns true, it represents event consumption. If false is returned, it means that no one at the lower level will handle it, and then go directly to the onTouchEvent method of the activity, which is also consistent with the previous consumption transmission.
Continue to look at the superDispatchTouchEvent method, and then go to the superDispatchTouchEvent method of PhoneWindow and the superDispatchTouchEvent of DecorView. 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);
    }

The copied code can be seen here. It passes through PhoneWindow and arrives at DecorView. DecorView is the root view of activity and the parent view of the view set by setcontentView. It inherits from FrameLayout. Therefore, the super.dispatchTouchEvent(event) method here actually goes to the dispatchTouchEvent method of viewgroup.

ViewGroup(dispatchTouchEvent)

    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
        if (onFilterTouchEventForSecurity(ev)) { 
            //  Check   for   interception, a field indicating whether to intercept 
            final boolean intercepted; 
            if (actionMasked == MotionEvent.ACTION_DOWN 
                    || mFirstTouchTarget != null) { 
              //FLAG_ DISALLOW_ The interrupt flag is set through requestDisallowInterceptTouchEvent                  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;
            }


          //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 code here intercepts some key code. The first is two conditions

  • actionMasked == MotionEvent.ACTION_DOWN
  • mFirstTouchTarget != null

If one of the conditions is met, you can continue. Execute the onInterceptTouchEvent method. Otherwise, you can directly intercept = true, indicating interception.
The first condition is obvious, which means that the current event bit is an ACTION_DOWN event
The second condition is a field. According to the following code, when a view consumes an event, the mFirstTouchTarget field will be assigned a value, otherwise it will be empty.

So what do you mean, when action_ When the down event occurs, the following code must be executed. When other events come, it depends on whether the current viewgroup consumes events. If the current viewgroup has consumed events and has not transmitted them to the child view, the mFirstTouchTarget field will be empty, so the following code will not be executed and all events will be consumed directly.

This is in line with the mechanism mentioned earlier:
Once a view starts intercepting, all subsequent events will be handled by it, and the onInterceptTouchEvent method will not be executed

However, if one of the two conditions is met, can onintercepttuchevent be executed? Not necessarily. Here is another judgment condition: disallowIntercept. This field is set by the requestDisallowInterceptTouchEvent method. As we will talk about later, it is mainly used for sliding conflict, which means that if the sub view tells you that you don't want you to intercept, you won't intercept and return false directly.

ok, continue to look at the source code. We learned from the previous content that if the viewgroup does not intercept events, it should be passed to the child view. Where did it pass? Continue to look at the code of 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 here. You can see the convenience of a sub view. If one of the two conditions is met, it will jump out. Otherwise, the dispatchTransformedTouchEvent method is executed. Let's look at these two conditions first:

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

You can't see anything from the name. Just look at the code:

 protected boolean canReceivePointerEvents() {         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 code Oh, that's what I mean. The canReceivePointerEvents method represents whether the view can accept click events, such as whether the animation is playing. The isTransformedTouchPointInView method represents whether the coordinates of the click event are above the area of the view.
ok, if all the conditions are met, execute 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);
            return handled;
        }
}

You should have guessed that the method of copying code actually executes child.dispatchTouchEvent(event). That is, the dispatchTouchEvent method of the next layer view starts the hierarchical transmission of events.

View(dispatchTouchEvent)

When you reach the view level, you will naturally execute the dispatchTouchEvent of the view and add the 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 here. You can see that you will first judge li.montouchlistener= Null, if not null, the onTouch method will be executed.
According to the result returned by the onTouch method, if it is false, the result is false, and onTouchEvent will be executed. This logic is also in line with the transmission mode we said before.
Finally, let's take a look at what the onTouchEvent of view 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:                     boolean prepressed = (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. You can know from the code that if CLICKABLE or long is set_ CLICKABLE, then the view will consume the event, execute the performClickInternal method, and then execute to the performClick method. This performClick method should be familiar to everyone. It is the method to trigger a click. In fact, the onClick method is executed internally.

     private boolean performClickInternal() { 
        notifyAutofillManagerOnClick();
        return performClick(); 
    } 

    public boolean performClick() { 
        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, and you can see almost the source code. In fact, there are many details inside. I won't explain them one by one here. You can study them when you are free.

Application for event distribution (requestDisallowInterceptTouchEvent)

Now that we have learned the event distribution mechanism, how will we apply it in our actual work? In fact, the most common problem is to solve sliding conflict. There are generally two solutions:

  • One is external interception: it is processed from the parent view and determines whether events are distributed to the child view according to the situation
  • One is internal interception: process from the child view and decide whether to block the parent view according to the situation. The key is the requestDisallowInterceptTouchEvent method.

The first method is to determine whether to return true or false in the onInterceptTouchEvnet method.
The second method is to use the requestDisallowInterceptTouchEvent method. This method means that the parent view should not intercept events. In the dispatchTouchEvent method, there is this flag bit: flag_ DISALLOW_ If the disallowaintercept field is true, the onInterceptTouchEvent method will not be executed, but false will be returned and the event will not be intercepted.
Upper Code:

     //External interception method: parent view.java          
    @Override 
    public boolean onInterceptTouchEvent(MotionEvent ev) { 
        boolean intercepted = false; 
       //Parent view interception 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;

   }

Copying code for external interception is very simple. It is to judge the conditions and then decide whether to intercept.

     //Parent view.java              
    @Override 
    public boolean onInterceptTouchEvent(MotionEvent ev) {         if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 
            return false; 
        } else { 
            return true; 
        } 
    }

    //Subview.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //Parent view interception 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);
    }

Copying code feels that internal interception is a little complicated. You have to rewrite the method of the parent view. Here's an analysis of why you want to write this:

  • Parent view action_ When down, you cannot intercept, because if you intercept, subsequent events have nothing to do with the child view
  • When the parent view other events, it should return true, indicating interception. Because the onInterceptTouchEvent method is called by flag_ DISALLOW_ It is controlled by the interrupt flag bit, so the child view will come to the onInterceptTouchEvent method only when it needs to be intercepted by the parent view. At this time, it is necessary to ensure that the method must be intercepted.

So far, the event distribution mechanism is almost the same.

end of document

Your favorite collection is my greatest encouragement!
Welcome to follow me, share Android dry goods and exchange Android technology.
If you have any opinions on the article or any technical problems, please leave a message in the comment area for discussion!

Topics: Android Back-end