[Android]View's event distribution mechanism (source code analysis)

Table of contents

1. Distribution object - MotionEvent

2. How to pass events

1. Delivery process

2. Source code analysis of event distribution

3. Main method:

4. Listener in event delivery

5. How to handle sliding conflicts with event distribution


1. Distribution object - MotionEvent


Event types are:

1. ACTION_DOWN-----Finger just touched the screen

2. ACTION_MOVE------finger moves on the screen

3. ACTION_UP------The moment when the finger is released from the screen

4. ACTION_CANCEL-----triggered when the event is intercepted by the upper layer

The main method of MotionEvent:

getX() Get the x-axis coordinate of the event (relative to the current view)
getY() Get the y-axis coordinate of the event (relative to the current view)
getRawX() Get the x-axis coordinate of the event (relative to the left vertex of the screen)
getRawY() Get the y-axis coordinate of the event (relative to the left vertex of the screen)

2. How to pass events


1. Delivery process

底层IMS->ViewRootImpl->activity->viewgroup->view

2. Source code analysis of event distribution

1. Activity's distribution process for click events

  • Activity#dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
//事件交给Activity所附属的Window进行分发,如果返回true,循环结束,返回false,没人处理
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
//所有View的onTouchEvent都返回false,那么Activity的onTouchEvent就会被调用
        return onTouchEvent(ev);
}
  • Window#superDispatchTouchEvent
 public abstract boolean superDispatchTouchEvent(MotionEvent event);
  • PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  • DecorView#superDispatchTouchEvent()
 public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
}
  • ViewGroup#dispatchTouchEvent()
   public boolean dispatchTouchEvent(MotionEvent ev) {
   
   

2. The distribution process of the top-level View to the click event

Explain the code in the dispatchTouchEvent() method of ViewGroup in sections

first paragraph:

Describes the logic of whether View intercepts click events

 // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//事件类型为down或者mFirstTouchTarget有值
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//询问是否拦截,方法返回true就拦截
                    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;//直接拦截了
            }

When the event type is down or mFirstTouchTarget has a value, the current event will not be intercepted, otherwise the event will be intercepted directly. So when does mFirstTouchTarget have a value? When the ViewGroup does not intercept the event and passes the event to the child element for processing, mFirstTouchTarget has a value and points to the child element. So when the event type is down and the event is intercepted, then mFirstTouchTarget is empty, which will make the following events move and up unable to meet the condition that mFirstTouchTarget has a value, and the onInterceptTouchEvent method cannot be called directly.

Special case: Set the flag bit FLAG_DISALLOW_INTERCEPT through the requestDisallowInterceptTouchEvent method, and the ViewGroup cannot intercept click events other than ACTION_DOWN. This flag bit cannot affect the ACTION_DOWN event, because when the event is ACTION_DOWN, the flag bit will be reset, which will cause the child View Setting this flag bit is invalid.

Summarize:

1. When ViewGroup decides to intercept the event, subsequent click events will be handed over to it for processing by default and its onInterceptTouchEvent method will no longer be called. 

2. When the ViewGroup does not intercept the ACTION_DOWN event, then the flag bit FLAG_DISALLOW_INTERCEPT makes the ViewGroup no longer intercept the event.

Second paragraph:

When the ViewGroup does not intercept the event, distribute the event to the sub-View to see which sub-View handles the event

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                       //对子元素进行排序
                        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);

                            // 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 (!child.canReceivePointerEvents()
                                    || !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;
                            }

Traverse all sub-elements of the ViewGroup to determine whether the sub-element is playing animation and whether the coordinates of the click event fall within the area of ​​the sub-element. If so, the click event can be received and the event will be passed to it for processing.

Let's take a look at the dispatchTransformedTouchEvent method

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ...
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            ...
}

dispatchTransformedTouchEvent actually calls the dispatchTouchEvent method of the child element.

If the dispatchTouchEvent method of the child element returns true, then mFirstTouchTarget will be assigned and jump out of the for loop

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

These lines of code complete the assignment of mFirstTouchTarget and terminate the traversal of the child elements. If the dispatchTouchEvent method of the child element returns false, the ViewGroup will distribute the event to the next child element.

In fact, the real assignment of mFirstTouchTarget is in the addTouchTarget method, and mFirstTouchTarget is a single linked list structure.

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

Third paragraph:

execution event

//当前View的事件处理代码
if (mFirstTouchTarget == null) {
          // No touch targets so treat this as an ordinary view.
           handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
//子View的事件处理代码
...

The third parameter of the dispatchTransformedTouchEvent method is null, and the super.dispatchTouchEvent method will be called, which is the dispatchTouchEvent method of View, so the click event is processed by View.

View's processing of click events

View (not including ViewGroup) is a separate element, has no child elements, and can only handle events by itself.

public boolean dispatchTouchEvent(MotionEvent event) {
   ...
   boolean result = false;
   ...
   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;
            }
     }
    ...
    return result;
}

First determine whether OnTouchListener is set. If the onTouch method of OnTouchListener returns true, the onTouchEvent method will not be called, otherwise the onTouchEvent method will be called.

public boolean onTouchEvent(MotionEvent event) {
           ...
           if ((viewFlags & ENABLED_MASK) == DISABLED
                && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                     setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                 return clickable;
          }
               ...
}

The View in the unavailable state will still consume click events

switch (action) {
    case MotionEvent.ACTION_UP:
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
         if ((viewFlags & TOOLTIP) == TOOLTIP) {
                  handleTooltipUp();
         }
         ...
         if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
         ...
              if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                           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;

When the ACTION_UP event occurs, the performClick method will be triggered. If the View is set with OnClickListener, its onClick method will be called inside the performClick method.

 private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }
public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        //关键代码,判断是否设置了onClickListener
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        ...
         
        return result;//最终返回执行结果
}

The source code implementation of the click event distribution mechanism has been analyzed.

3. Main method:


1. dispatchTouchEvent: Used to distribute events. If the event can be delivered to the current View, then this method will be called. The returned result is affected by the onTouchEvent of the current View and the dispatchTouchEvent method of the subordinate View, indicating whether to consume the current event.

2. onInterceptTouchEvent: used to determine whether to intercept an event, if the current View intercepts an event, then in the same event sequence, this method will not be called again, and the returned result indicates whether to intercept the current event.

3. onTouchEvent: used to process click events, and the returned result indicates whether to consume the current event. If not, in the same event sequence, the current View cannot receive the event again.

4. requestDisallowInterceptTouchEvent: Generally used in child Views, requiring the parent View not to intercept events.

5. dispatchTransformedTouchEvent: If the child is not empty, send it to the dispatchTouchEvent of the child, otherwise send it to yourself.

4. Listener in event delivery


The order of onTouch , performClick and onClick calls and the effect of onTouch return value?

When a View needs to process an event, in the dispatchTouchEvent method of View, if OnTouchListener is set, the onTouch method of OnTouchListener will be called. When the onTouch method returns true, onTouchEvent will not be called. When the onTouch method returns false, onTouchEvent The method is called, enter the performClick method in the onTouchEvent method, and judge whether to set the onClickListener in the performClick method, and if the onClickListener is set, then the onClick method is called, and the performClick method returns true. If the onClickListener is not set, the performClick method is returns false.

In general, the order of method calls is

5. How to handle sliding conflicts with event distribution


Sliding conflict definition : When there are two layers of Views inside and outside that can respond to the event, who will decide the event.

Types of sliding conflicts : 1. When the sliding directions of the inner and outer layers of View are inconsistent

                         2. When the sliding direction of the inner and outer layers is the same

                         3. Superposition of two situations

Solutions:

Internal interception: dispatchTouchEvent+dispatchTransformedTouchEvent

Rewrite the dispatchTouchEvent method of the child element

The down event is distributed to child elements, and the move event depends on the conditions. If the conditions are not met, the event will be handed over to the child element for processing. If the condition is met, the processing event of the child element will be canceled, and then the event will be handed over to the parent element

public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                //down事件,父容器不要拦截我
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此类点击事件) {
                   //父容器拦截我
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

(When the move event occurs, enter the first block of code, call intercepted = onInterceptTouchEvent(ev), we set in the onInterceptTouchEvent method to return true if it is not a down event, so intercepted is true, then the second block of code will not be executed, enter the third Block code, because intercepted is true, so cancelChild is true, cancel the child element event execution, call the dispatchTransformedTouchEvent method, cancel is true->

event.setAction(MotionEvent.ACTION_CANCEL)->handled = child.dispatchTouchEvent(event)

Set mFirstTouchTarget to empty, so when the next move event comes, mFirstTouchTarget is empty, intercepted is true in the first piece of code, the second piece of code is not executed, and the third piece of code goes dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS), that is, the event processing code (parent element) of the current View)

Override the onInterceptTouchEvent method of the parent element

When it is a down event, return false, because in the dispatchTouchEvent method of ViewGroup, when it is a down event, the resetTouchState() method will be called, and the resetTouchState() method will reset the state and reset mGroupFlags, which will As a result, the previous parent.requestDisallowInterceptTouchEvent(true) is useless, so we return false when setting the down event in the onInterceptTouchEvent method, because onInterceptTouchEvent will definitely be executed during the down event.

   public boolean onInterceptTouchEvent(MotionEvent event) {

        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            super.onInterceptTouchEvent(event);
            return false;
        } else {
            return true;
        }
    }

External interception: onInterceptTouchEvent

The click event is first intercepted by the parent container. If the parent container needs it, it will be intercepted, and if it is not needed, it will not be intercepted.

public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (满足父容器的拦截要求) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

Guess you like

Origin blog.csdn.net/weixin_63357306/article/details/128629042