Event distribution mechanism of View

What did the VIew incident include

The event of View actually refers to MotionEvent, that is, a series of actions such as clicking, sliding and lifting the screen. It has the following four event types

  1. ACTION_DOWN: the finger just touched the screen
  2. ACTION_MOVE: move your finger 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

Under normal circumstances, a finger touching the screen will trigger a series of click events, mainly in the following two cases:

  • Click the screen and release it. The event sequence is: action_ DOWN -> ACTION_ UP
  • Click the screen to slide for a while and then release it. The event sequence is: action_ DOWN -> ACTION_ MOVE -> … -> ACTION_ MOVE -> ACTION_ UP
    We can write the following code for observation
public class MotionEventActivity extends AppCompatActivity {

    private static final String TAG = "MotionEvent";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btn_motion_event = findViewById(R.id.btn_motion_event);
        btn_motion_event.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                switch (motionEvent.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.d(TAG, "MotionEvent: ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.d(TAG, "MotionEvent: ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.d(TAG, "MotionEvent: ACTION_UP");
                        break;
                }
                return false;
            }
        });
        
        btn_motion_event.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d(TAG, "onClick: button Be clicked");
            }
        });
    }
}

So, action_ Must up be the end of an event? Not necessarily. After the test of the above code, if we accidentally end the click event, such as pressing the menu key, home key or lock screen, there will be no ACTION_UP.

Event distribution rules

Three event distribution methods

When a MotionEvent is generated, the system needs to pass the event to a specific View, and the transfer process is the process of distribution. The distribution process of click events is completed by three very important methods: dispatchTouchEvent(), onInterceptTouchEvent() and onTouchEvent(), which are introduced one by one below.

  • public boolean dispatchTouchEvent(MotionEvent event)
    It is used for event distribution. If the event can be passed to the current View, the dispatchTouchEvent method will be called.
    Return value: indicates whether the current event has been consumed. It may be consumed by the onTouchEvent method of the View itself or by the dispatchTouchEvent method of the child View. Returning true indicates that the event is consumed and the event is terminated. Returning false means that neither the View nor the child View has consumption events, and the onTouchEvent method of the parent View will be called
  • public boolean onInterceptTouchEvent(MotionEvent ev)
    It is called inside the dispatchTouchEvent () method to determine whether to intercept an event. When a ViewGroup receives the MotionEvent event event sequence, it will first call this method to determine whether to intercept it. In particular, this is a unique method of ViewGroup. View does not intercept methods
    Return value: whether to intercept the current event. Returning true indicates that the event has been intercepted. The event will no longer be distributed downward, but will call the onTouchEvent method of the View itself. If false is returned, the event will be distributed down to the dispatchTouchEvent method of the child View without interception.
  • public boolean onTouchEvent(MotionEvent ev)
    Call in dispatchTouchEvent to handle the click event
    Return value: return true to indicate that the event is consumed and the current event is terminated. If false is returned, it means that the event has not been consumed, and the onTouchEvent method of the parent View will be called. At the same time, in the same event sequence, the current View cannot receive events again.

The above three methods can use the following pseudo code to express the relationship between them:

public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;//Is the event consumed
        if (onInterceptTouchEvent(ev)){//Call onInterceptTouchEvent to determine whether to intercept the event
            consume = onTouchEvent(ev);//If intercepted, call its own onTouchEvent method
        }else{
            consume = child.dispatchTouchEvent(ev);//Do not intercept the dispatchTouchEvent method that calls the child View
        }
        return consume;//The return value indicates whether the event is consumed. If true, the event terminates. If false, call the onTouchEvent method of the parent View
    }

There may be two questions:

  1. Why does View have a dispatchTouchEvent method?
  2. Why does View have no onInterceptTouchEvent method?

On the first question, a View can register many listeners, such as click, long press, touch event (onTouch), and the View itself also has onTouchEvent method. Then the question comes, who should manage so many event related methods, so the View will also have dispatchTouchEvent method.
As for the second one, let's take a look at a diagram to clarify the structure of View

We can see that ViewGroup and View can be included under ViewGroup, but View cannot. That is, if an event is distributed to a specific View, it only needs to choose whether to process or not, and there is no need to intercept, because it certainly does not need to be distributed downward.

Delivery rule

For a root ViewGroup, after the click event is generated, it will be passed to it first, and then its dispatchTouEvent will be called. If the onInterceptTouchEvent method of the ViewGroup returns true, it means that it wants to intercept the current event, and then the event will be handed over to the ViewGroup for processing, that is, its onTouchEvent method will be called, If the onInterceptTouchEvent method of this ViewGroup returns false, it means that it does not intercept the current event. At this time, the current event will continue to be passed to its child element, and then the dispatchTouEvent method of the child element will be called. This is repeated until the event is finally handled.
Use an event distribution flow chart to illustrate:

When a View needs to handle events, if it sets onTouchListener, the onTouch method in onTouchListener will be called back. How to handle the event depends on the return value of onTouch. If false is returned, the onTouchEvent method of the current View will be called; If true is returned, the onTouchEvent method will not be called. It can be seen that setting onTouchListener for View has higher priority than onTouchEvent. In the onTouchEvent method, if onClickListener is currently set, its onClick method will be called. It can be seen that onClickListener, which we usually use, has the lowest priority, that is, it is at the end of event transmission

When a click event is generated, its delivery process follows the following sequence: Activity - > Window - > View. The event is always delivered to the Activity first, the Activity is delivered to the Window, and finally the Window is delivered to the top-level View. After receiving the event, the top-level View will distribute the event according to the event distribution mechanism. Here we are considering a case, If the ontouchevent of a View returns false, the ontouchevent of its parent container will be called. If all elements do not handle this event, the event will eventually be handed over to the Activity for processing, that is, the ontouchevent method of the Activity will be called.

As for the event transmission mechanism, we can draw the following conclusions. According to these conclusions, we can better understand the whole transmission mechanism:

  1. The same event sequence refers to a series of time from the moment when the finger touches the screen to the moment when the finger leaves the screen. This event sequence starts with the down event, contains an indefinite number of move events, and ends with the up event.
  2. Under normal circumstances, an event sequence can only be intercepted and consumed by one View. For this reason, please refer to (3), because once an element intercepts an event, all events in the same event sequence will be directly handed over to it for processing. Therefore, events in the same event sequence cannot be processed by two views at the same time, but it can be done by special means, For example, a View forcibly passes events that should be handled by itself to other views for processing through onTouchEvent.
  3. Once a View decides to intercept, this event sequence can only be processed by it (if the event sequence can be passed to it), and its onInterceptTouchEvent will not be called again. This is also easy to understand, that is, when a View decides to intercept an event, the system will directly hand over other methods in the same event sequence to it for processing, so it will no longer call onInterceptTouchEvent of the View to ask whether it wants to intercept.
  4. Once a View starts processing events, if it does not consume ACTION_DOWN event (onTouchEvent returns false), then other events in the same event sequence will not be handed over to it for processing, and the event will be handed over to its parent element for processing again, that is, onTouchEvent of the parent element will be called. It means that once an event is handed over to a View for processing, it must be consumed, otherwise the remaining events in the same event sequence will not be handed over to it for processing. This is like the superior handing over a matter to the program ape. If the matter is not handled properly, the superior will not dare to hand over the matter to the program ape in a short time. The two are similar.
  5. If View does not consume action_ For other events other than down, the click event will disappear. At this time, the onTouchEvent of the parent element will not be called, and the current View can continue to receive subsequent events. Finally, these disappeared click events will be passed to the Activity for processing.
  6. ViewGroup does not intercept any events by default. The onInterceptTouchEvent method of ViewGroup in Android source code returns false by default
  7. View has no onInterceptTouchEvent method. Once a click event is passed to it, its onTouchEvent method will be called.
  8. onTouchEvent of View will consume events by default (return true), unless it is not clickable (clickable and longClickable are false at the same time) The longClickable attribute of View is false by default, and the clickable attribute depends on different situations. For example, the clickable attribute of Button is true by default, while the clickable attribute of TextView is false by default
  9. The enable property of View is not affected. The default return value of onTouchEvent is. Even if a View is in disable state, as long as one of its clickable or longClickable is true, its onTouchEvent will return true
  10. onClick occurs on the premise that the current View is clickable and it receives down and up events.
  11. The event delivery process is from the outside to the outside, that is, events are always delivered to the parent element first, and then distributed by the parent element to the child View. The event distribution process of the parent element can be intervened in the child element through the requestDisallowInterceptTouchEvent method, but the action_ Except for the down event.

Event distribution source code

Distribution of activities and windows

When a click operation occurs, the event is first passed to the current Activity, and the event is distributed by the Activity's dispatchTouchEvent. The specific work is completed by the Window inside the Activity

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
            //The default is an empty function. Don't worry
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

Now analyze the above code. First, the event is handed over to the Window attached to the Activity for distribution. If true is returned, the whole event cycle ends. If false is returned, it means that the event is not handled, and all on Touchevent of View return false, then on Touchevent of Activity will be called.

Window is an abstract class, and the superDispatchTouchEvent method of window is an abstract method. Its only implementation class is PhoneWindow

public boolean superDispatchTouchEvent(MotionEvent event) {
		return mDecor.superDispatchTouchEvent(event);
	}

PhoneWindow passes events directly to DecorView.
We can use ((ViewGroup) getwindow() getDecorView(). findViewById(android.R.id.content)). Getchildat (0) is to get the internal View through the Activity. This mDecor is obviously getwindow() The View returned by getdecorview (), and the View we set through setContentView is a child of it. In short, the event is now passed to the top-level ViewGroup.

Distribution of ViewGroup

After the event reaches the top-level ViewGroup, the dispatchTouchEvent method of the ViewGroup will be called. The logic is as follows: if the underlying ViewGroup intercepts the event, that is, onInterceptTouchEvent returns true, the event will be handled by the ViewGroup. If the top-level ViewGroup does not intercept the event, the event will be passed to its child View on the click event chain. At this time, the dispatchTouchEvent of the child View will be called. This cycle completes the whole event distribution.
In addition, it should be noted that ViewGroup does not intercept click events by default, and its onInterceptTouchEvent returns false.

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

As can be seen from the above code, ViewGroup will judge whether to intercept the current event in the following two cases: the event type is down or mFirstTouchTarget= null. mFirstTouchTarget can be understood in this way. When the ViewGroup does not intercept events and hand them over to child elements, mFirstTouchTarget will be assigned, that is, mFirstTouchTarget is not empty. Once the event is intercepted by the current ViewGroup, mFirstTouchTarget is always empty. When the move and up events arrive, the ViewGroup will not judge whether to intercept, and the onInterceptTouchEvent() method will not be called. Other events in the same sequence will be handled by it.
There is another special case here, that is flag_ DISALLOW_ The interrupt flag bit, which is set through the requestDisallowInterceptTouchEvent method, is generally in the sub View. Even if the event has been distributed, the child element can still call the requestDisallowInterceptTouchEvent method of the parent element to set the flag_ DISALLOW_ The interrupt flag bit to determine whether to intercept events from the parent element. Except for the down event. Because when the ViewGroup distributes events, if it is a down event, it will reset the flag_ DISALLOW_ The flag bit of intersect will cause the flag bit set in the sub View to be invalid.

Next, let's look at the processing of event downward distribution 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 (!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;
                            }

First, traverse all child elements of ViewGroup, and then judge whether the child elements can receive click events. Whether the click event can be received is mainly measured by two points: whether the sub element is playing the animation and whether the coordinates of the click event fall within the area of the sub element. If a child element meets these two conditions, the event will be passed to it for processing. The dispatchTransformedTouchEvent method is actually calling the dispatchTouchEvent method of the child element. There is the following section inside it, as shown below. Since the child passed above is not null, it will directly call the dispatchTouchEvent method of the child element, In this way, the event is handled by the child element, thus completing a round of event distribution.

if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }

If the dispatchTouchEvent of the child element returns true, indicating that the child element has processed the event, mFirstTouchTarget will be assigned and the for loop will jump out, as shown below:

	newTouchTarget = addTouchTarget(child, idBitsToAssign);
	//Complete the assignment of mFirstTouchTarget in the addTouchTarget() method
	alreadyDispatchedToNewTouchTarget = true;
	break;

If the event is not handled properly after traversing all child elements, there are two cases: ViewGroup has no child elements; The child element handles the click event, but dispatchTouchEvent returns false, generally because the child element returns false on onTouchEvent. At this time, ViewGroup will handle the click event by itself.

View's handling of click events

First look at the dispatchTouchEvent method of View

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

The process of View handling click events is relatively simple. Because View is a separate element and has no child elements, it cannot pass events down, so it can only handle events by itself. The View will first judge whether the OnTouchListener is set. If the OnTouchListener method returns true, the onTouchEvent method will not be called. Therefore, the OnTouchListener has higher priority than the onTouchEvent. The advantage of this is to facilitate the external processing of click events.

Next, analyze the onTouchEvent method

if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    ......
                    if (!mHasPerformedLongPress) {
                            // 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)) {
                                    performClick();
                                }
                            }
                        }
                        ......
                    }
                    break;
			}
			......
            return true;
            //The default value returned is true In this way, multiple touch events can be executed.
        }

It can be seen that as long as CLICKABLE and long_ If touchevent is true, it will return a consumption event, that is, touchevent. When the up event occurs, the performClick method will be triggered. If the View sets OnClickListener, performClick will call its onClick method.
Long of View_ The CLICKABLE attribute is false by default, while the CLICKABLE attribute is true by default. However, the CLICKABLE attribute of a specific View is not necessarily true. Specifically, the CLICKABLE attribute of a CLICKABLE View is true, such as bottom. The CLICKABLE attribute of a non CLICKABLE View is false, such as TextView.. These two properties can be set through setClickable and setLongClickable. In addition, setOnClickListener and setOnLongClickListener will automatically set these two properties of View to true.

Problem exploration

  1. When there is a View in the ViewGroup, if the ViewGroup is only in ACTION_MOVE interception, talk about how each action is distributed

First, the down event will pass through the dispatchTouchEvent of the ViewGroup, then the oninterceptotouchevent of the ViewGroup, and finally the dispatchTouchEvent of the view. At this time, mFirstTouchTarget is not empty, and then to move. First, the dispatchTouchEvent of the ViewGroup, and then the oninterceptotouchevent of the ViewGroup. Because it is intercepted during the move process, Then go to the action of dispatchTouchEvent in view_ Cancel, then
mFirstTouchTarget is set to nul, so subsequent move and up events will only follow the dispatchTouchEvent and onTouchEvent of ViewGroup.

  1. View is consumed in onTouchEvent, and then drag your finger to move away from other viewgroups

Events have been consumed in onTouchEvent of View, so mFirstTouchTarget is not empty. Therefore, no matter where the finger moves, other events will be consumed in this View

  1. The difference between onTouch and onTouchEvent of View

onTouch is a callback method in setOnTouchListener, which takes precedence over onTouchEvent. If the onTouch method returns true, the onTouchEvent method will not be triggered

  1. When is the onClick method of View triggered, and what is the difference between it and onTouch

onClick is the action in the onTouchEvent consumption event_ Up trigger. onTouch is triggered in dispatchTouchEvent, so onTouch should take precedence over onClick event. We can mask the onClick method by returning true through onTouch.

Keywords: Android UI

Added by jlh3590 on Sun, 27 Feb 2022 12:00:31 +0200