Now that we have learned about [Event distribution of view], we will continue to study the event distribution and processing of View, so as to find the cause of event conflict and its solution.

I. Conceptual cognition

1) Event type:

The events that handle MotionEvent() in the view's onTouchEvent(method) are: DOWN: the event when the finger touches the screen UP: the event when the finger leaves the screen MOVE: the event when the finger moves on the screen CANCEL: The event is triggered when the event is interceptedCopy the code

2) Classes and methods of event distribution

From the source point of view, the process of event distribution:  activity--->dispatchTouchEvent() PhoneWindow --->superDispatchTouchEvent() DecorView --->superDispatchTouchEvent() ViewGroup--->dispatchTouchEvent() view--->dispatchTouchEvent() view--->onTouchEvent()Copy the code

3) Event Conflict:

There is only one event, but multiple objects are being processed, and the actual object being processed is not the expected object, so there is a conflict;Copy the code

That is, event conflicts occur when an event can be handled by more than one view (such as scrolling nesting), as well as when a view has more than one way to handle an event (such as touch and click events).

Second, the process of event distribution

Before we learned about event distribution, some states or tokens were handled by bitwise operators, so here’s a quick look at bitwise operators:

& more bit value is 1, the result of the bit is 1 | as long as there is a is 1, the results of the bit is 1 ~ the current exchange 0 1, or 0 to 1, 1 to 0Copy the code

2.1) processing and distribution process of DOWN event:

Viewgroup. Java dispatchTouchEvent(MotionEvent EVCopy the code

2.1.1) Reset mGroupFlags

MGroupFlags &= mGroupFlags & (~FLAG_DISALLOW_INTERCEPT);Copy the code
if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); } // reset mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;Copy the code

2.1.2) Whether the event is intercepted

Replace the following expression with the value of mGroupFlags reset in the previous step: Final Boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)! = 0; Final Boolean disallowIntercept = (mGroupFlags & (~FLAG_DISALLOW_INTERCEPT)& FLAG_DISALLOW_INTERCEPT)! = 0; DisallowIntercept is false; OnInterceptTouchEvent (EV) is called to determine whether it has been intercepted. This method can be overridden in a custom view.Copy the code
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; }Copy the code

2.1.3) Event processing and distribution

1) When intercepted, the onInterceptTouchEvent(ev) method returns true

A) Skip the following if (! canceled && ! Intercepted) conditions;Copy the code

If (mFirstTouchTarget ==null), mFirstTouchTarget==null, At that time, would be the to dispatchTransformedTouchEvent (ev, canceled, null, TouchTarget. ALL_POINTER_IDS);

Note: The second argument canceled is false and the third argument is null

if (! canceled && ! intercepted) {// ignore this} // Dispatch to touch targets. If (mFirstTouchTarget == null) {// No touch targets so treat this  as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else {// ignore}}Copy the code
B) enter dispatchTransformedTouchEvent () method:Copy the code
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; final int oldAction = event.getAction(); If (cancel | | oldAction = = MotionEvent. ACTION_CANCEL) {/ / conditions are not set up here, Perform any necessary doubling and dispatch. If (child == null) {handle = handled = super.dispatchTouchEvent(transformedEvent); } else {// done.transformedevent.recycle (); return handled; }Copy the code

Description:

Since the third argument passed in is NULL, it goes straight to the bottom of the method and calls super.dispatchTouchEvent(transformedEvent); And the super of the current viewGroup is the View; This makes it obvious that the View’s dispatchTouchEvent() method is called.

C) dispatchTouchEvent () methodCopy the code

Description:

1, Set the OnTouchListener, which is called back to the listener’s onTouch() method.

六四屠杀

The onTouch() method returns true to indicate that the current view has consumed the event, and the onTouch() method returns false to enter the View's onTouchEvent()Copy the code

2. If OnTouchListener is not set, the view’s onTouchEvent() will be displayed

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; }Copy the code
D) View.java onTouchEvent(), which is where the actual event is handledCopy the code
//onTouchEvent() public Boolean onTouchEvent(MotionEvent) { All the clickable below are true 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_DOWN: // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick( ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); } break; } return true if the down event has been consumed; } return false; }Copy the code

2) When not intercepted: the onInterceptTouchEvent(ev) method returns false

A) The if condition skipped in the interception state above is executed when no interception is made

Description:

1. MChildrenCount Specifies the number of direct ChildViews in the viewgroup, excluding childViews from childViews

2, the loop traverse childView, if (dispatchTransformedTouchEvent (ev, false, child, idBitsToAssign)) event distribution childView again in this condition;

Here the second argument is false and the third argument child is not null, which is different from the down event

AddTouchTarget (Child, idBitsToAssign); addTouchTarget(Child, idBitsToAssign); In the event view list (the neat thing about this is that when a move, up event happens, you don’t have to look up the event again, you just look up the list)

if (! canceled && ! intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)  || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount ! = 0) { 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 (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(); } if (newTouchTarget == null && mFirstTouchTarget ! = null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next ! = null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; }}}Copy the code

B) into the dispatchTransformedTouchEvent again () method, which can perform the following code in the else branch, Child.dispatchtouchevent () will be called, and the child will be the third view passed in, which will recursively traverse all nodes below the current view, childView, until it finds where the event is handled.

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } handled = child.dispatchTouchEvent(transformedEvent); }}Copy the code

Now that the distribution and processing of the Down event is complete, let’s look at the processing and distribution of the Move event.

2.2). Distribution and processing of MOVE events

If (mFirstTouchTarget == NULL) {if (mFirstTouchTarget == null) {if (mFirstTouchTarget == null); It goes into the else branch:

The execution of the while loop is, the if (alreadyDispatchedToNewTouchTarget && target = = newTouchTarget) this condition is actually processing has been distributed, avoid to distribute again

1. If the view has already been distributed, it is returned directly

2, if there is no distribution, again into the dispatchTransformedTouchEvent () method, the need to focus on his second parameter cancelChild, critical look at the value of the intercepted

final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;

CancelChild is true if intercepted is blocked above, false otherwise.

{ // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target ! = null) { final TouchTarget next = target.next; /** newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; } */ if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; }Copy the code

CancelChild is true

The following code is basically the same as distributing and handling donw events. If there is no childView node, the view’s dispatchTouchEvent() is handled, and if there is, the recursive call child.dispatchTouchEvent(event) is performed.

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; } // The following code is omitted}Copy the code

CancelChild is false

This is the same as event handling and distribution for Down. Until called to the View's onTouchEvent() methodCopy the code
 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;    
 }
Copy the code

5. View onTouchEvent() handles move events

public boolean onTouchEvent(MotionEvent event) { switch (action) { case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } final int motionClassification = event.getClassification(); final boolean ambiguousGesture = motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE; int touchSlop = mTouchSlop; if (ambiguousGesture && hasPendingLongPressCallback()) { if (! pointInView(x, y, touchSlop)) { // The default action here is to cancel long press. But instead, we // just extend the timeout here, in case the classification // stays ambiguous. removeLongPressCallback(); long delay = (long) (ViewConfiguration.getLongPressTimeout() * mAmbiguousGestureMultiplier); // Subtract the time already spent delay -= event.getEventTime() - event.getDownTime(); checkForLongClick( delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); } touchSlop *= mAmbiguousGestureMultiplier; } // Be lenient about moving outside of buttons if (! pointInView(x, y, touchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) ! = 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } final boolean deepPress = motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS; if (deepPress && hasPendingLongPressCallback()) { // process the long click action immediately removeLongPressCallback(); checkForLongClick( 0 /* send immediately */, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS); } break; }}Copy the code

2.3) Processing and distribution of UP events

Now that we know how to handle DOWN and MOVE events, we believe that it is much easier to handle UP events. Let’s focus on the view onTouchEvent() method to handle UP events

public boolean onTouchEvent(MotionEvent event) { switch (action) { case MotionEvent.ACTION_MOVE: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } if (! clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } 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 (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed  // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } 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(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (! post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; }}Copy the code
  • Pay special attention to
performClickInternal(); This is actually the listener that handles our own implementation of OnClickListener. Therefore: TouchListener is called back on Down, while clickListener is handled on Up, so when we add both TouchListener and clickListener to a view, if we want clickListener to be valid, You need to return false in the onTouch callback method of touchListener to allow the event to continue distribution, otherwise it will not be distributed.Copy the code

Conclusion:

When a touch event (down) occurs, if the event is intercepted, the view processes the event. Otherwise, the view and its child are recursively iterated to find the view that handled the event, and the event is added to the list of the target view. When the next event (move) occurs, It is distributed directly in the target view’s linked list, and finally handled in the View’s onTouchEvent();

When the View sets touchListener, it is called back to the Listener when a Down event occurs. The Listener returns true, and the event is not delivered again. Otherwise, the event continues to be delivered.

When a View sets a clickListener, it is called back to the Listener when an UP event occurs, and the event is handled in the listener callback.

Note:

If the view is a leaf node, then if it doesn’t handle down events, it doesn’t handle move,up events either;

During event distribution and processing, the viewGroup is mainly responsible for event distribution, while the View is responsible for event processing.

If the event is intercepted, then the event can no longer be passed to its childView, i.e. it is the last view, and the event needs to be handled by itself;

Thus it can be seen that the handling of event conflict is as follows:

Internal interception: handles event conflicts in ChildView. External interception: handles event conflicts in ParentView.Copy the code

ParentView can rob events from ChildView, whereas childView cannot rob events from parentView. Once the view has the event, it’s up to the view to decide who handles the event. Note that childview refers to the direct childview of the current view, not the childview of the childview.

The last

If you study Android together, you can follow my [Github] — ❤️ [Github], where I sort out the basic and advanced knowledge points of Android, ❤️, and will do regular maintenance and updates. Pay attention to study with me!