banner
fwrite

fwrite

好好生活
twitter
github
email

Android View Event Dispatch

Event Passing#

Event Dispatching.drawio

Events are passed from Activity to View layer by layer.

Concept of Event Dispatching#

It is mainly accomplished through 3 methods.

Key Dispatch MethodsFunctionReturn Significance
Dispatch Touch Event: BooleanTriggered by the upper layer View, passed to the target ViewThe return result is influenced by the parent and child Views (true: handled) onTouchEvent dispatchTouchEvent
onInterceptTouchEvent: BooleanUsed in the current View to determine whether to intercept an eventThe return result indicates whether the event is intercepted (true: intercepted)
Touch Event: BooleanThe current View has intercepted and started processingThe return result indicates whether the event is consumed (true: handled)
public boolean dispatchTouchEvent(MotionEvent e) {
    bool isEventConsume = false;

    if(onInterceptTouchEvent(e)) {     
        isEventConsume = onTouchEvent(e);
    } else {
        isEventConsume = child.dispatchTouchEvent(e);
    }

    return isEventConsume;
}

It can be seen that the dispatch order is dispatchTouchEvent -> onInterceptTouchEvent, deciding the subsequent path based on whether the event is consumed. The recursive call of dispatchTouchEvent continues until it finds the View that consumes the event. The View is a tree structure; if the View has intercepted the click event, it will trigger onTouch. If onTouch does not consume the event, it will be passed to the onTouchEvent method, where the onClick event will be present.

Activity Receives Events#

//Activity.java
 
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // Here, getWindow refers to the PhoneWindow class
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}


//PhoneWindow.java
    
private DecorView mDecor;

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

//DecorView.java
//DecorView inherits from FrameLayout, and FrameLayout does not override 
//dispatchTouchEvent method, so it must look for its parent
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

// ViewGroup.java
// ViewGroup inherits from View
// ViewGroup overrides dispatchTouchEvent, so no need to continue to View
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
}

Event Handling#

Through the above analysis, we know how the click event is passed to DecorView#ViewGroup. Here we will further analyze the ViewGroup click event. First, let's understand a few MotionEvents:

MotionEvent EventActionOthers
ACTION_DOWNFinger downCan be intercepted or not
ACTION_UPFinger upEvent ends
ACTION_MOVESliding on screenWill be triggered multiple times
ACTION_CANCELEvent canceledTriggered when the event is intercepted by the upper layer

ViewGroup Handling - ChildView Intercepts ACTION_DOWN#

The Down event will only be triggered once (single touch, multiple touches will not be just once). Interception means that the ViewGroup handles the event itself and will not dispatch it to ChildView. Each click event starts with Action Down; please pay attention to the following annotations, and it mainly does the following (here we will first list the main handling items):
Clear previous events: Note resetTouchState method, which will be executed when the ViewGroup#dispatchTouchEvent event is ACTION_DOWN.

Note

Clear previous events: Note resetTouchState method, which will be executed when the ViewGroup#dispatchTouchEvent event is ACTION_DOWN.

// ViewGroup.java
    
// Start from this ViewGroup, passing down to the target of the click
private TouchTarget mFirstTouchTarget;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {

        // Get the current action
        final int action = ev.getAction();
        // Perform an AND operation with the Mask to get the real Action
        final int actionMasked = action & MotionEvent.ACTION_MASK;


        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Execute reset only during Action Down
            
            // @ Track cancelAndClearTouchTargets method
            cancelAndClearTouchTargets(ev);
            
            // @ Track resetTouchState method
            resetTouchState();
        }

    }

    ...

    return handled;
}

private void resetTouchState() {
    // @ See clearTouchTargets method
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    
    // Clear FLAG_DISALLOW_INTERCEPT, allowing ViewGroup to intercept
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

// Clear all events of the first clicked View chain
private void clearTouchTargets() {
    // TouchTarget is a singly linked list
    TouchTarget target = mFirstTouchTarget;
    if (target != null) {
        do {
            TouchTarget next = target.next;
            // Recycle TouchTarget for reuse
            target.recycle();
            target = next;
        } while (target != null);
        
        // Set member mFirstTouchTarget to null 
        mFirstTouchTarget = null;
    }
}

Whether to call ViewGroup#onInterceptTouchEvent method: There are two conditions, plus a FLAG check:

  • Currently is ACTION_DOWN event
  • A ChildView has already handled this click event (if a child View handles the event, it will assign a value to mFirstTouchTarget)
  • Whether the current ViewGroup is prohibited from intercepting (generally when ViewGroup receives an event, if it is not prohibited from intercepting, it will execute the onInterceptTouchEvent method) ACTION_DOWN
// ViewGroup.java
    
// Start from this ViewGroup, passing down to the target of the click
private TouchTarget mFirstTouchTarget;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {

        // Get the current action
        final int action = ev.getAction();
        // Perform an AND operation with the Mask to get the real Action
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Initialize FLAG_DISALLOW_INTERCEPT during Action down

        final boolean intercepted;
        // There are two checks here to determine whether to call ViewGroup's own onInterceptTouchEvent method
        // 1. Currently is ACTION_DOWN 
        // 2. A child View has already handled this click event 
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // Check whether interception is prohibited
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {

                // Call its own onInterceptTouchEvent method
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

    }

    ...

    return handled;
}

If the ViewGroup itself does not intercept, it will recursively call ChildView: executing ViewGroup#dispatchTransformedTouchEvent to dispatch to each ChildView for handling. If the ChildView consumes the event, it returns true, breaking the loop; otherwise, it queries the next ChildView.

// ViewGroup
    
// Start from this ViewGroup, passing down to the target of the click
private TouchTarget mFirstTouchTarget;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {

        final int action = ev.getAction();
        // Perform an AND operation with the Mask to get the real Action
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Initialize FLAG_DISALLOW_INTERCEPT during Action down

        final boolean intercepted;
        // Check whether the ViewGroup can intercept the event

        // Currently assume ViewGroup does not intercept
        if (!canceled && !intercepted) {
            ...

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                ...

                if (newTouchTarget == null && childrenCount != 0) {
                    ...
                    // Reorder the ChildViews in this ViewGroup
                    final ArrayList<View> preorderedList = 
                                    buildTouchDispatchChildList();
                    ...

                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);

                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        ...

                        // Whether the animation can receive pointer events
                        // Whether the click is within the child element isTransformedTouchPointInView 
                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // Get the View being clicked 
                        // New View events will return non-null
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // Give it the new pointer in addition to the ones it is handling.
                            // Child View has received the click
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }


                        // The key is in the dispatchTransformedTouchEvent method
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // ChildView successfully handles the event

                            ...

                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();

                            // addTouchTarget method
                            // will assign value to mFirstTouchTarget
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            // Break the loop
                            break;
                        }


                    }
                }

            }
        }

    }

    ...

    return handled;
}

ViewGroup#dispatchTransformedTouchEvent method: ViewGroup will use this method to pass the event to ChildView. If the passed child is null, it calls the ViewGroup's parent dispatchTouchEvent; if it is not null, it calls the specified View's dispatchTouchEvent (in the current case, a View object is passed, so it will jump to the specified View#dispatchTouchEvent method).

// ViewGroup.java

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

    final boolean handled;

    ...

    if (child == null) {
        // If the Child view is null, return to the ViewGroup's Parent
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        ...

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    return handled;
}

ViewGroup#addTouchTarget: assigns the member mFirstTouchTarget
ViewGroup#addTouchTarget: equips the member mFirstTouchTarget

// ViewGroup.java

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        // Get a reusable TouchTarget object

        final TouchTarget target = TouchTarget
                    .obtain(child, pointerIdBits);
        
        // Link to the previous event's View
        target.next = mFirstTouchTarget;
        
        // The first View that handles the click event
        mFirstTouchTarget = target;
        return target;
    }

After the ChildView receives and intercepts the event (through the method), it will assign a value to ViewGroup#mFirstTouchTarget and break the loop. After the ChildView processes the event, it returns to ViewGroup to continue processing.

// ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    ...

    if (onFilterTouchEventForSecurity(ev)) {


        if (mFirstTouchTarget == null) {
            ...
        } else {
            ... 

            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;

            while (target != null) {
                final TouchTarget next = target.next;
                
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    
                    // Dispatch the event
                    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;
            }

        }

    }

    ...

    return handled;
}

ViewGroup Handling - ChildView Does Not Intercept ACTION_DOWN#

ViewGroup ChildView intercepts the event: the handling method is the same (as in the previous section), here we mainly look at the different parts. This event is handled by the ViewGroup itself (dispatchTransformedTouchEvent method).

// ViewGroup.java

    // Start from ViewGroup
    private TouchTarget mFirstTouchTarget;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {

          
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;


            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Execute reset only during Action Down

                cancelAndClearTouchTargets(ev);

                resetTouchState();
            }

        }

        ...

        return handled;
    }

Clear previous events: Note the resetTouchState method, which will be executed in ViewGroup#dispatchTouchEvent when the event is ACTION_DOWN. Whether to call ViewGroup#onInterceptTouchEvent method: There are two conditions, plus a FLAG check: currently is event ACTION_DOWN, and a ChildView has already handled this click event (if a child View handles the event, it will assign a value to mFirstTouchTarget), whether the current ViewGroup is prohibited from intercepting (generally when ViewGroup receives an event, if it is not prohibited from intercepting, it will execute the onInterceptTouchEvent method) ACTION_DOWN. If the ViewGroup itself does not intercept, it will recursively call ChildView: executing ViewGroup#dispatchTransformedTouchEvent to dispatch to each ChildView for handling.

// ViewGroup
    
    // Start from ViewGroup
    private TouchTarget mFirstTouchTarget;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            ...
                
            if (!canceled && !intercepted) {
                // Currently, there are no ChildViews to handle this event
            }
        }
        ...
    }

ChildView receives but does not intercept the event, so ViewGroup#mFirstTouchTarget is null.

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ...

        if (onFilterTouchEventForSecurity(ev)) {

            ...

            // Currently, there are no ChildViews handling the event
            if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                ...
            }

        }

        ...

        return handled;
    }


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

        ...

        if (child == null) {
            // Return to the ViewGroup's Parent dispatchTouchEvent
            handled = super.dispatchTouchEvent(transformedEvent);

        } else {
            ...
        }
        ...
        return handled;
    }

View Receives Events - Handling ACTION_UP#

The View first dispatches to onTouch; if it is not handled, it dispatches to onTouchEvent. The order is as follows:

  • First executes the onTouch interface; if it has been handled, the onTouchEvent will not receive the event onTouch
  • If onTouch does not handle this event, it will be the turn of the View's onTouchEvent to handle the event onTouchEvent
// View.java

    public boolean dispatchTouchEvent(MotionEvent event) {
        ...

        boolean result = false;

        ...

        if (onFilterTouchEventForSecurity(event)) {
            ...

            // Dispatch the event to onTouch
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    // Check if the View is enabled
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            
            // Analyze onTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ...

        return result;
    }

In the View#onTouchEvent method, you can see the calls to onClick and onLongClick interfaces.

// View.java

    // PerformClick represents the click event of this View
    private PerformClick mPerformClick;

    public boolean onTouchEvent(MotionEvent event) {

        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();


        // As long as one of CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE is set,
        // this View is clickable
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;


        // Click event proxy TouchDelegate
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }


        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {

                case MotionEvent.ACTION_UP:
                    ...

                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;

                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ...

                        // Long press task mHasPerformedLongPress
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // Remove long press Callback
                            removeLongPressCallback();
                            // Only perform click actions if we were in the pressed state
                            if (!focusTaken) {

                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }

                                // Analyze performClickInternal
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                        ...
                    }
                    mIgnoreNextUpEvent = false;
                    break;
            }
        }
    }

Here you can see that the View's click event is sent through a Handler for the Click task (Runnable), so it will not block the click event.

// View.java

    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        
        if (attachInfo != null) {
            // Place the Click task into the Handler 
            return attachInfo.mHandler.post(action);
        }
        getRunQueue().post(action);
        return true;
    }

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

    public boolean performClick() {
        notifyAutofillManagerOnClick();
        
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            // Execute OnClick
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }

ViewGroup - ACTION_DOWN#

  1. Determine whether the event is intercepted by ViewGroup:
    If ViewGroup intercepts the event -> directly go to 3
    If ViewGroup does not intercept the event -> first go to 2, then 3
  2. If ViewGroup does not intercept and one of the ChildViews intercepts the event and processes it
    ViewGroup#newTouchTarget is assigned a value
    Dispatch to its own View (calls its parent) super.dispatchTouchEvent
// ViewGroup.java

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    ...

    mLastTouchDownX = ev.getX();
    mLastTouchDownY = ev.getY();
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
}
  1. Event dispatch and handling: there are two situations
    All ChildViews do not intercept this event, so mFirstTouchTarget is null, equivalent to the last View.
if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
        TouchTarget.ALL_POINTER_IDS);
}

If a ChildView intercepts, then mFirstTouchTarget is not null, and the while loop only runs once because the ChildView has handled it (handled during dispatch).

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    handled = true;
}

ACTION_MOVE - View Received MOVE#

The Move event will still first go through the DecorView, this ViewGroup. When executing the event, it is necessary to know a few things about ACTION_MOVE. The ViewGroup does not clear the click Flag, the TouchTarget type mFirstTouchTarget element is not null (because a ChildView element has already processed), the judgment flow chart of the ACTION_MOVE event is as follows. It mainly checks whether the ViewGroup intercepts the event (the following assumes not intercepting). It will not enter the reset stage.

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            ...
        }
    }

Check whether the current ViewGroup prohibits interception; if not, call its own method to check for interception FLAG_DISALLOW_INTERCEPT onInterceptTouchEvent.

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        ...

        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // Check whether ViewGroup is prohibited from intercepting
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

            // Assume ViewGroup does not intercept
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
    }

It is not an event, so it will not enter the initial event dispatch stage ACTION_DOWN.

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

            ...

        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
      
        if (!canceled && !intercepted) {

            // Not a Down event
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
               ...

            }
        }

        ... 
    }

The key point of ACTION_MOVE is the event dispatch.

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ...

        if (mFirstTouchTarget == null) {
            ...

        } else {
          
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;

                // alreadyDispatchedToNewTouchTarget is false
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;

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

                    // Analyze dispatchTransformedTouchEvent to dispatch to ChildView
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    ...
                }
                predecessor = target;
                target = next;
            }
        }
    }

Next, through the dispatchTransformedTouchEvent method: dispatch the event to ChildView's dispatchTouchEvent ACTION_MOVE.

// ViewGroup.java 

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) 

        ...

        if (child == null) {
            ...
        } 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);
        }
    }

ACTION_MOVE - ViewGroup Intercepts MOVE#

First, understand the current situation: a ChildView has already processed the ACTION_DOWN event, and we customize a ViewGroup and copy the ViewGroup#onInterceptTouchEvent method to return true during ACTION_MOVE to intercept.

// Custom ViewGroup.java

public class MyViewGroup extends View {

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(ev.getAction() == MotionEvent.ACTION_MOVE) {
            // ViewGroup consumes the event itself
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

}

The ACTION_MOVE event occurs multiple times. The first ACTION_MOVE (coming from the ParentView) aims to:

  1. Cancel the ChildView event (change the event to ACTION_CANCEL)
  2. Set ViewGroup#mFirstTouchTarget to null; at this point, the ParentView does not handle the event.
    As a result, the ChildView will receive an ACTION_CANCEL event (the event is canceled, and thereafter, the ViewGroup will handle the event).
/**
 * ViewGroup.java
 * 
 * Since overriding the onInterceptTouchEvent method makes the Move event true, 
 * cancelChild will also be true, 
 * the cancel parameter passed to the dispatchTransformedTouchEvent method will be true,
 * 
 * At this point, the event will be changed to ACTION_CANCEL, and the ChildView will receive the ACTION_CANCEL event
 */ 
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ...

        if (mFirstTouchTarget == null) {
            ...

        } else {

            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // The first time entering ACTION_MOVE
                    // intercepted is true, so cancelChild = true
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;

                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }

                    // mFirstTouchTarget = next, next is null
                    // so mFirstTouchTarget = null
                    if (cancelChild) {
                        if (predecessor == null) {
                            // next is null, so mFirstTouchTarget is null
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        ...
    }


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

        final boolean handled;

        final int oldAction = event.getAction();
        // cancel is true
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {

            // Change the event to ACTION_CANCEL
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                ...
            } else {
                // Dispatch the event to ChildView as cancel
                // Triggered when the event is intercepted by the upper layer
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        ...
            
        return handled;
    }

The second ACTION_MOVE (coming from the ParentView), since mFirstTouchTarget is null, the ParentView will not dispatch.
As a result, the ParentView will handle the event itself.

//ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

        // In the first ACTION_MOVE, mFirstTouchTarget is set to null
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            ...
        } else {
            intercepted = true;
        }

        // intercepted = true 
        if (!canceled && !intercepted) {
            ... 
        }


        if (mFirstTouchTarget == null) {
            // ChildView is null
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {

            ...
        }

        ...
}


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

    final boolean handled;

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

Event Conflict#

Why does a conflict occur? Because there can only be one event, if the responding component is not the desired View object, it can be called an event conflict. It can be handled in two ways:

  • Internal interception method: requires ChildView#dispatchTouchEvent to handle & requires cooperation to modify the parent container (ParentView) onInterceptTouchEvent interception.
  • External interception method (more commonly used): only requires ParentView to handle.

Internal Interception - ChildView Handling#

In ChildView, use the requestDisallowInterceptTouchEvent method to control the FLAG_DISALLOW_INTERCEPT element, so that the ParentView will not dispatch the event downwards. The internal interception method is for the ChildView to request the ParentView not to intercept the event at the appropriate time using the method requestDisallowInterceptTouchEvent.

public class MyView extends View {

    int dispatchX, dispatchY;

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

        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // Request ParentView not to intercept
                getParent().requestDisallowInterceptTouchEvent(true);
                break;

            case MotionEvent.ACTION_UP:
                break;

            case MotionEvent.ACTION_MOVE:
                int deltaX = x - dispatchX;
                int deltaY = y - dispatchY;
                
                // Let the event be handled by ViewGroup
                if(Math.abs(deltaX) > Math.abs(deltaY)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }

        dispatchX = x;
        dispatchY = y;

        return super.dispatchTouchEvent(event);
    }

}

Override the ParentView's method to set it to not intercept the event during ACTION_DOWN, so that the event can be passed to ChildView in onInterceptTouchEvent.

public class MyViewGroup extends ViewGroup {

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(ev.getAction() == MotionEvent.ACTION_DOWN) {
            // ViewGroup does not intercept
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }

}

Details of requestDisallowInterceptTouchEvent: Why requestDisallowInterceptTouchEvent cannot control the ACTION_DOWN event.

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {

                cancelAndClearTouchTargets(ev);
                // See resetTouchState
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {    // mFirstTouchTarget != null

                // Control FLAG_DISALLOW_INTERCEPT
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // If true, the ParentView will not dispatch downwards
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

            //
        }
    }


    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

From the above, it can be seen that in ViewGroup during ACTION_DOWN, the resetTouchState() method is used, which clears this Flag, causing the ACTION_DOWN event to be dispatched. FLAG_DISALLOW_INTERCEPT

// ViewGroup.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        ...

        final boolean intercepted;

        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // After clearing, FLAG_DISALLOW_INTERCEPT is empty
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            
            if (!disallowIntercept) {
                // Determine ViewGroup#onInterceptTouchEvent
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        }
        ...
    }

Note

In ViewGroup, override onInterceptTouchEvent to let ViewGroup return false during ACTION_DOWN (not intercept), so that ACTION_DOWN can be dispatched and passed down to ChildView, while other actions will be intercepted.

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(ev.getAction() == ACTION_DOWN) {
            return false;
        }
        return true;
    }

External Interception - ParentView Handling#

Handling event interception by ParentView is simpler; just override ParentView#onInterceptTouchEvent to decide which time (condition) to intercept the event.

public class MyExternalViewGroup extends ViewGroup {

    int interceptX, interceptY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercept = false;
        boolean defaultIntercept = super.onInterceptTouchEvent(event);

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch(event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - interceptX;
                int deltaY = y - interceptY;
                
                // Horizontal movement, intercept
                if(Math.abs(deltaX) > Math.abs(deltaY)) {
                    intercept = true;
                } else {
                    intercept = false;
                }
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;
                
            default:
                intercept = defaultIntercept;
                break;
        }

        interceptX = x;
        interceptY = y;

        return intercept;
    }
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.