1, the preface

Android learning for a period of time, the need to do more will inevitably encounter the sliding conflict problem, such as in a ScrollView to nested a map View, at this time the touch to move the map or zoom in and out of the map will become not accurate or even no response, this is encountered sliding conflict, Scrolling up and down in ScrollView conflicts with the touch gesture for the map. If you want to solve sliding conflicts, you have to refer to Android’s event distribution mechanism. Only when you understand event distribution, you can solve sliding conflicts with ease.

2. Methods related to event distribution mechanism

There are three main methods related to the Android event distribution mechanism:

  • Public Boolean dispatchTouchEvent(MotionEvent ev)
  • Public Boolean onInterceptTouchEvent(MotionEvent ev)
  • Public Boolean onTouchEvent(MotionEvent ev)

Here’s how these three methods work in activities, ViewGroups, and Views:

Relevant methods Activity ViewGroup View
dispatchTouchEvent yes yes yes
onInterceptTouchEvent no yes no
onTouchEvent yes yes yes

All three methods return a Boolean type that distributes intercepts and responses to events differently depending on what is returned. There are generally three methods that return true, false, and super reference the parent class counterpart.

DispatchTouchEvent returns true: the changed event is no longer distributed at this layer and has been consumed in the event distribution itself. DispatchTouchEvent returns false: the event is no longer distributed at this layer and is consumed by the onTouchEvent method of the upper control.

OnInterceptTouchEvent returns true: intercepts the event and sends it to onTouchEvent of this layer control for processing. OnInterceptTouchEvent returns false: the event is not intercepted and is successfully distributed to a child View. And handled by the child View’s dispatchTouchEvent.

OnTouchEvent returns true: onTouchEvent consumes the event after processing the event. At this point the event terminates, and no further delivery will take place. OnTouchEvent returns false: the event will continue to be passed to the upper View after processing in onTouchEvent, and the upper View’s onTouchEvent will be processed.

In addition, there is another method that is often used:

  • public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)

It’s used by the child View to tell the parent View not to intercept events. Let’s write a simple Demo to look at event distribution and delivery:

Simple log Demo:

The code here simply customizes two ViewGroups and a View and prints logs in their event distribution methods to see the order of calls. All related distribution methods return super methods. For example: myViewGroupa.java:

public class MyViewGroupA extends RelativeLayout {
    public MyViewGroupA(Context context) {
        super(context);
    }
    public MyViewGroupA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_UP");
                break;
        }        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
                break;
        }        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
                break;
        }        returnsuper.onTouchEvent(event); }}Copy the code

The rest of the code is similar. Here’s the layout from Acitivity:

<? xml version="1.0" encoding="utf-8"? > <com.example.sy.eventdemo.MyViewGroupA xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/viewGroupA"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context=".MainActivity">

    <com.example.sy.eventdemo.MyViewGroupB
        android:id="@+id/viewGroupB"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_centerInParent="true"
        android:background="@android:color/white">

        <com.example.sy.eventdemo.MyView
            android:id="@+id/myView"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_centerInParent="true"
            android:background="@android:color/holo_orange_light" />
    </com.example.sy.eventdemo.MyViewGroupB>
</com.example.sy.eventdemo.MyViewGroupA>
Copy the code

The Activity layout hierarchy in the Demo:

 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onTouchEvent:ACTION_DOWN
 D/MainActivity: onTouchEvent:ACTION_DOWN
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MainActivity: onTouchEvent:ACTION_MOVE
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MainActivity: onTouchEvent:ACTION_UP
Copy the code

Activity–>MyViewGroupA–>MyViewGroupB–>MyView Sends events from the top down in response order: MyView–>MyViewGroupB–>MyViewGroupA–>Activity responds to consumption from bottom up

At the same time, there is a problem found in the log:

  • Question 1: Why do ACTION_DOWN events have complete run logs for intercepts and responses from Activity to ViewGroup to View? Why don’t ACTION_MOVE and ACTION_UP events have complete run logs?

Then test the premise of requestDisallowInterceptTouchEvent method used. Now add a property android:clickable=”true” to MyView layout file. At this point in the run click print log looks like this:

/ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ACTION_DOWN events -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- D/MainActivity: dispatchTouchEvent: ACTION_DOWN D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN D/MyView: dispatchTouchEvent:ACTION_DOWN D/MyView: OnTouchEvent ACTION_DOWN / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ACTION_MOVE events -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- D/MainActivity: dispatchTouchEvent:ACTION_MOVE D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE D/MyView: dispatchTouchEvent:ACTION_MOVE D/MyView: OnTouchEvent: ACTION_MOVE / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ACTION_UP event -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- D/MainActivity: dispatchTouchEvent:ACTION_UP D/MyViewGroupA: dispatchTouchEvent:ACTION_UP D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP D/MyViewGroupB: dispatchTouchEvent:ACTION_UP D/MyViewGroupB: onInterceptTouchEvent:ACTION_UP D/MyView: dispatchTouchEvent:ACTION_UP D/MyView: onTouchEvent:ACTION_UPCopy the code

Now ACTION_MOVE and ACTION_UP events are logged. MyViewGroupB onInterceptTouchEvent: MyViewGroupB onInterceptTouchEvent

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
                return false;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
                return true;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
                return true;
        }
        return false;
    }
Copy the code

Block ACTION_MOVE and ACTION_UP events, not block ACTION_DOWN events, and then view the log when running:

/ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ACTION_DOWN events -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- D/MainActivity: dispatchTouchEvent:ACTION_DOWN D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN D/MyView: dispatchTouchEvent:ACTION_DOWN D/MyView: OnTouchEvent ACTION_DOWN / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ACTION_MOVE events -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- D/MainActivity: dispatchTouchEvent:ACTION_MOVE D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE D/MyViewGroupB: OnInterceptTouchEvent: ACTION_MOVE / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ACTION_UP event -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - D/MainActivity: dispatchTouchEvent:ACTION_UP D/MyViewGroupA: dispatchTouchEvent:ACTION_UP D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP D/MyViewGroupB: dispatchTouchEvent:ACTION_UP D/MyViewGroupB: onTouchEvent:ACTION_UP D/MainActivity: onTouchEvent:ACTION_UPCopy the code

ACTION_MOVE and ACTION_UP events are passed to MyViewGroupB and are not passed to MyView. Then call requestDisallowInterceptTouchEvent method in MyView onTouchEvent method in notify the parent container don’t intercept events.

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }
Copy the code

Run again to view the log:

/ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ACTION_DOWN events -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- D/MainActivity: dispatchTouchEvent:ACTION_DOWN D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN D/MyView: dispatchTouchEvent:ACTION_DOWN D/MyView: OnTouchEvent ACTION_DOWN / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ACTION_MOVE events -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- D/MainActivity: dispatchTouchEvent:ACTION_MOVE D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE D/MyView: dispatchTouchEvent:ACTION_MOVE D/MyView: OnTouchEvent: ACTION_MOVE / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ACTION_UP event -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - D/MainActivity: dispatchTouchEvent:ACTION_UP D/MyViewGroupA: dispatchTouchEvent:ACTION_UP D/MyViewGroupB: dispatchTouchEvent:ACTION_UP D/MyView: dispatchTouchEvent:ACTION_UP D/MyView: onTouchEvent:ACTION_UPCopy the code

You can see that ACTION_MOVE and ACTION_UP events are passed to MyView and that neither ViewGroup has an onInterceptTouchEvent method. Apparently requestDisallowInterceptTouchEvent methods play a role. But two new problems have emerged.

  • Question 2: Why will it be setclickable="true"afterACTION_MOVEandACTION_UPThe event will be executed, right?
  • Question 3:requestDisallowInterceptTouchEventHow to tell the parent View not to intercept events, and whyonInterceptTouchEventMethod doesn’t execute either?

To figure out these questions can only look for answers in the source code.

3, event distribution mechanism source code

One concept before we look at the source code: sequence of events

When we talk about events, we generally refer to the process from the touch of a finger to the screen and away from the screen. In this process, multiple events will be generated, usually starting with ACTION_DOWN, with multiple ACTION_MOVE in between, and ending with ACTION_UP. We call an ACTION_DOWN–>ACTION_MOVE–>ACTION_UP process a sequence of events.

There is an internal class TouchTarget in ViewGroup. This class encapsulates the View consuming the event as a node, making it possible to store the DOWN, MOVE, and UP events of a sequence of events as a single linked list. The ViewGroup also has a member of type TouchTarget mFirstTouchTarget that points to this single linked header. Clear the list at the start of each DOWN event, obtain a TouchTarget after successfully consuming the event using the TouchTarget.obtain method, pass in the View of the consuming event, and insert it into the single linked list header. Subsequent MOVE and UP events can determine if there was a previous View that could consume the event by judging the mFirstTouchTarget.

TouchTarget TouchTarget

private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin; private static int sRecycledCount; public static final int ALL_POINTER_IDS = -1; // All ones // The touched child view. // The combined bit mask of pointer idsfor all pointers captured by the target.
        public int pointerIdBits;

        // The next target inPublic TouchTarget next; public TouchTarget next; privateTouchTarget() {
        }

        public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
            if (child == null) {
                throw new IllegalArgumentException("child must be non-null");
            }

            final TouchTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new TouchTarget();
                } else {
                    target = sRecycleBin;
                    sRecycleBin = target.next;
                     sRecycledCount--;
                    target.next = null;
                }
            }
            target.child = child;
            target.pointerIdBits = pointerIdBits;
            return target;
        }

        public void recycle() {
            if (child == null) {
                throw new IllegalStateException("already recycled once");
            }

            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                } else{ next = null; } child = null; }}}Copy the code
The dispatchTouchEvent method in the Activity:

Start with the Activity’s dispatchTouchEvent method, which is called when an event is generated:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
Copy the code

The onUserInteraction method, which is an empty implementation of the Activity, is called when the Home or Back key is pressed under the current Activity. This is not the point, the point here is to pay attention to the ACTION_DOWN event, the judgment of the ACTION_DOWN type of event is very important in the logic of event delivery, because each click event starts with an ACTION_DOWN event, so the ACTION_DOWN event is used as the mark of a new click event.

Next, the return of the entire method is determined by the return value of getWindow().superDispatchTouchEvent(ev) in the second if judgment.

The dispatchTouchEvent method returns true if the getWindow().superDispatchTouchEvent(ev) method returns true, otherwise it returns based on the value returned by the onTouchEvent method in the Activity.

The onTouchEvent method in the Activity:

Let’s start with the onTouchEvent method in the Activity:

  public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }
Copy the code

The onTouchEvent method depends on the Window’s shouldCloseOnTouch method to decide whether to return the result and finish the current Activity. Go to the abstract class Window and look at the shouldCloseOnTouch method:

 /** @hide */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        if(mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) && peekDecorView() ! = null) {return true;
        }
        return false;
    }
Copy the code

This is a hide method that determines whether the current Event is of ACTION_DOWN type, whether the current Event click coordinate is outside the range, and so on. If true, it will return to the onTouchEvent method to close the current Activity.

After that, go back to the dispatchTouchEvent method and the only thing left is the getWindow().superDispatchTouchEvent(ev) method to see when it returns true and when it returns false. Here getWindow gets the Window object in the Activity and calls Widnow’s superDispatchTouchEvent(EV) method, which is not in the abstract Window class, to look at its implementation of PhoneWindow.

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

SuperDispatchTouchEvent method calls again mDecor. SuperDispatchTouchEvent method, here is outer mDecor DecorView, superDispatchTouchEvent method:

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
Copy the code

DecorView inherits from FrameLayout, FrameLayout doesn’t override dispatchTouchEvent so it just calls dispatchTouchEvent from its parent class ViewGroup.

ViewGroup dispatchTouchEvent method:

Through these calls, the event eventually moves from the Activity to the PhoneWindow to the DecorView and finally to the dispatchTouchEvent method of the ViewGroup, Next go to the ViewGroup and look at its dispatchTouchEvent method.

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
		......
	
        boolean handled = false;
        if(onFilterTouchEventForSecurity(ev)) { / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- a code block -- 1 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // Handle an initial down.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 forthe previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); } / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - the code block - 1 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 2 a code block -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / Checkfor 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 actionin 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; } / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 2 - a code block out -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / If intercepted, start normal event dispatch. Alsoif there is already
            // a view that is handling the gesture, do normal event dispatch.
            if(intercepted || mFirstTouchTarget ! = null) { ev.setTargetAccessibilityFocus(false);
            }

            // Check forCancelation. / / check whether the event is cancelled final Boolean canceled = resetCancelNextUpFlag (this) | | actionMasked = = MotionEvent.ACTION_CANCEL; // Update list of touch targetsfor pointer down, ifneeded. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) ! = 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget =false; / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - a code block 3 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --if(! canceled && ! intercepted) { ......if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);
                    final int childrenCount = mChildrenCount;
                    if(newTouchTarget == null && childrenCount ! = 0) { finalfloat x = ev.getX(actionIndex);
                        final float y = 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(! canViewReceivePointerEvents(child) || ! 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 pointerin 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 indexfor (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; }}} / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 3 - a code block out -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - a code block 4 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / Dispatch to touch the targets, the if (mFirstTouchTarget == null) {// If mFirstTouchTarget is null, No child View consumes this event // No touch targets so treat this as an ordinary View view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 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; 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; }} / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 4 - a code block out -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --... return handled; }Copy the code

The dispatchTouchEvent method of the ViewGroup is quite long, it’s a lot of code even though I’ve omitted some of the code, and there’s a lot of if-else judgment in the code, so it’s easy to get lost between if and else. So here we break it up into four pieces of code. But before we look at these four pieces of code let’s look at the first if judgment in the dispatchTouchEvent method:

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)){
            ......
        }
Copy the code

Handled here initialization is dispatchTouchEvent method finally return values, onFilterTouchEventForSecurity this method to filter the think unsafe events, method in the main window is obscured the view and judgment, DispatchTouchEvent method all distributed logic to under the premise of onFilterTouchEventForSecurity return to true, otherwise returned directly handled is false. Let’s look at the first code:

 final int action = ev.getAction();
 final int actionMasked = action & MotionEvent.ACTION_MASK;
 // Handle an initial down.
 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();
 }
Copy the code

ACTION_DOWN is considered to be the start of a new sequence of events, so reset the touch state and empty the mFirstTouchTarget list. If you look at this in the resetTouchState method, in addition to resetting some of the states, it also calls the clearTouchTargets method to clear the list.

    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    
    /**
     * Clears all touch targets.
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if(target ! = null) {do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }    
Copy the code

Then we see code block 2:

            // Check forInterception. // Check for interception of the event final Boolean intercepted; // Is the ACTION_DOWN event or mFirstTouchTarget entered without nullif
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! = null) {/ / continue to determine whether the call requestDisallowInterceptTouchEvent (trueFinal Boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)! = 0; // Set the disallowIntercept flag totrue,! DisallowIntercept isfalse
                if(! DisallowIntercept) {return intercepted = onInterceptTouchEvent(ev) according to the ViewGroup nInterceptTouchEvent(ev) method; ev.setAction(action); // restore actionin case it was changed
                } else{// if the stop interception flag is set, no interception intercepted =false; }}else{ // There are no touch targets and this action is not an initial down // so this view group continues to intercept Touches. // Instead of an ACTION_DOWN event or mFirstTouchTarget=null, touches intercepted =true;
            }
Copy the code

The main purpose of this code is to determine whether to intercept the event, intercepted is the interception flag, true means interception, false means not interception. If the event type is DOWN or mFirstTouchTarget does not equal null (mFirstTouchTarget does not equal null), if this condition is met, enter if. Otherwise, set intercepted to false and do not intercept. In the if judgment FLAG_DISALLOW_INTERCEPT this tag, the tag is in requestDisallowInterceptTouchEvent set () method. Here to requestDisallowInterceptTouchEvent (true) method to take a look at:

@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); }}Copy the code

See requestDisallowInterceptTouchEvent method in different operations, according to disallowIntercept mGroupFlags default is 0, 0 FLAG_DISALLOW_INTERCEPT x80000, If the pass is set to true, mGroupFlags is 0x80000, and FLAG_DISALLOW_INTERCEPT is 0x80000, which is not 0. If false is passed in, the final bit is 0. That is to say, the incoming call requestDisallowInterceptTouchEvent method true cause disallowIntercep to true, leading to the if conditions are not met, makes the intercepted to false to intercept events at this time. Otherwise, the onInterceptTouchEvent(EV) method is called in the if code block and is intercepted based on the return value.

           if(! canceled && ! intercepted) { // If the event is targeting accessiiblity focus we give it to the // view that has accessibility focus andifit does not handle it // we clear the flag and dispatch the event to all children as usual. // We are looking up the accessibility focused host to avoid keeping // state since these events are very rare. View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; // Check whether the event type is DOWNifCode block, where the three markers correspond to the single touch DOWN multi-touch DOWN and mouse movement events respectivelyif (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in casethey // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); Final int childrenCount = mChildrenCount; // Loop the child View to find the child View that can respond to the event and distribute the eventif(newTouchTarget == null && childrenCount ! = 0) { finalfloat x = ev.getX(actionIndex);
                    final float y = 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; } // The child View cannot accept the event or the event click will skip the loop if it is not in the child Viewif(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false);
                            continue; Return null newTouchTarget = getTouchTarget(child);if(newTouchTarget ! = null) { // Child is already receiving touch within its bounds. // Give it the new pointerinIn addition to the ones it is handling. // Not null indicates that the view has handled this event, indicating that multi-touch, Just add a pointer newTouchTarget. PointerIdBits | = idBitsToAssign;break; } resetCancelNextUpFlag(child); / / call dispatchTransformedTouchEvent method distributed events to Viewif (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 indexfor (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break; }}}else{ mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); / / dispatchTransformedTouchEvent returnstrueNewTouchTarget = addTouchTarget(Child, idBitsToAssign); // Set the already distributed tags totrue
                            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 is null and mFirstTouchTarget is not null, no child View was found to respond to the consumption event. Next if (newTouchTarget == null &&). 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

Next, block 3. In this long code, the first if determines whether the event is not blocked and cancelled, and the second if determines whether the event type is DOWN, satisfies the DOWN event that is not blocked and cancelled. Next ViewGroup will cycle like son find click events in its internal View and be able to accept the child View of events, again by calling dispatchTransformedTouchEvent method will be distributed to the child View event handling, return true son View consumer success, The addTouchTarget method is called, which uses the TouchTarget.obtain method to obtain a TouchTarget node containing the View, add it to the list header, and set the distributed tag to true. Let’s look at code block 4:

// Dispatch to touch targets. // This indicates that the event is not accepted after iterating through all sub-views, or that the event is not DOWN, or that the event has been blocked or cancelledif(mFirstTouchTarget = = null) {/ / mFirstTouchTarget is empty that no children View the incident response consumption / / all call dispatchTransformedTouchEvent method distribute events // Note that the third argument is passed null, // No touch targets so treat this as an ordinary event view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }else{// mFirstTouchTarget is not null, indicating that a child View can consume the event, consuming the previous DOWN event, Dispatch this event to the View // Dispatch to touch targets, including the new touch targetif we already
                // dispatched to it.  Cancel touch targets if necessary.
                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; // The target. Child is the View that responded to the previous consumption, and the event is returned to itif (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

Previously, block 3 handled the distribution of DOWN events that were not intercepted and cancelled. What about other MOVE, UP, and other types of events? And what happens if you walk through a child View and there’s no View that accepts the event? These events are handled and distributed in block 4. First of all determine whether mFirstTouchTarget is empty, empty the event that no child View consumption, and then call dispatchTransformedTouchEvent method distribute events, Here on the third parameter View passed null dispatchTransformedTouchEvent methods, an approach for this no children View can handle consumption situation of events, just call the ViewGroup super. DispatchTouchEvent method, That’s dispatchTouchEvent of the View, treats the ViewGroup like a View, passes the event to the ViewGroup. Specific see dispatchTransformedTouchEvent method in this code:

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

DispatchTransformedTouchEvent method that child in the incoming View is empty. Call the super dispatchTouchEvent method to distribute events, is the distribution method, the View class isn’t empty call child View method, Child.dispatchtouchevent dispatchTouchEvent dispatchTouchEvent dispatchTouchEvent dispatchTouchEvent

At this point, the distribution process in ViewGroup is over. To summarize the process: First filter out unsafe events. Then if the event type is DOWN and the event is considered to start a new sequence of events, clear the TouchTarget list and reset the relevant flag bits (block 1). Then determine whether to block the event. First, if it is a DOWN event or not a DOWN event but mFirstTouchTarget is not equal to null (if mFirstTouchTarget is equal to null, no View has consumed a DOWN event before, at the end of block 3, If a child View consumes a DOWN event, the addTouchTarget method will be called to obtain a TouchTarget that saves the child View and add it to the mFirstTouchTarget header. Otherwise, the onInterceptTouchEvent method of ViewGroup will be called to determine whether to intercept according to the return (block 2). Next didn’t be cancelled if the event is not intercept and DOWN events, just iterate over the child View of ViewGroup found in its range and can accept events of View, through dispatchTransformedTouchEvent method distributed event to the View, Then insert the TouchTarget containing the child View into the header of the list using the addTouchTarget method (block 3). If mFirstTouchTarget is null, no View can accept the event. If mFirstTouchTarget is null, no View can accept the event. Call dispatchTransformedTouchEvent method will events to their own, not empty call dispatchTransformedTouchEvent method is the same, but are distributed to the child the event View.

The onInterceptTouchEvent method of the ViewGroup:
public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
Copy the code

The onInterceptTouchEvent method returns false by default. The onInterceptTouchEvent method returns false by default. Returns true only if the event source type is mouse and the DOWN event is mouse click button and scroll bar gesture. By default, the ViewGroup onInterceptTouchEvent method returns false, meaning it does not intercept events by default.

ViewGroup onTouchEvent method:

The onTouchEvent method is not overridden in a ViewGroup, so the onTouchEvent method that calls a ViewGroup actually calls its parent View’s onTouchEvent method.

View’s dispatchTouchEvent method:

The default View class dispatchTouchEvent method is executed when an event is dispatched to a child View or handled by the ViewGroup itself:

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

Again, I’m going to skip some code here and just look at the key thing, first of all, I’m going to do the same event security filtering as ViewGroup, and then I’m going to check whether mOnTouchListener is empty, not empty and the View is ENABLED and available, The onTouch method of mOnTouchListener is called. If the onTouch method returns true indicating that the event has been consumed, the result flag is changed to true so that it does not follow the if. If mOnTouchListener is not set or the onTouch method returns false, the onTouchEvent method continues to be called. The onTouch method of mOnTouchListener takes precedence over onTouchEvent. If mOnTouchListener is set in the code and onTouch returns true, the event is consumed in onTouch. The onTouchEvent method will not be called again.

/ / the mOnTouchListener is often set in the code the OnTouchListener mMyView. SetOnTouchListener (new View.OnTouchListener() {@override public Boolean onTouch(View v, MotionEvent event)trueThe event is consumed and the onTouchEvent method is never called againreturn true; }});Copy the code
View’s onTouchEvent method:
public boolean onTouchEvent(MotionEvent event) { / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - a code block 1 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - finalfloat x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            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; } / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - a code block 1 -- -- -- -- -- - the -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 2 a code block -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the if (mTouchDelegate! = null) { if (mTouchDelegate.onTouchEvent(event)) { return true; }} / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 2 a code block after -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - a code block 3 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the if (clickable | | (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: 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 actionsif 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) {// Call OnClickListener performClick(); }}}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;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if(! clickable) { checkForLongClick(0, x, y);break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // 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) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } // Be lenient about moving outside of buttons if (! pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) ! = 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; } / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - a code block after 3 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- return false. }Copy the code

There’s a lot of code in the onTouchEvent method, too, but most of it is logic that responds to events and has little to do with the event distribution process. Let’s look at the first code block:

    final float x = event.getX();
        final floaty = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); // It is CONTEXT_CLICKABLE, CONTEXT_CLICKABLE, and CLICKABLEtruefinal boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // Check whether the current View is availableifThe code blockif((viewFlags & ENABLED_MASK) == DISABLED) {// If the UP event is invoked and the View is PRESSEDsetPressed set tofalse
            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 doesnRespond to the clickable state. Respond to the clickable state if the View is not available. }Copy the code

In code block 1, we first check whether the View is clickable, and then determine if the View is unavailable and return clickable directly, but we do nothing. The default clickable value of a View is false, and the default clickable value of a View is true. The default clickable value of a Button is true, and the default clickable value of a TextView is false.

        if(mTouchDelegate ! = null) {if (mTouchDelegate.onTouchEvent(event)) {
                return true; }}Copy the code

In block 2, an mTouchDelegate touch proxy is judged, and if it is not null, the proxy’s onTouchEvent is called in response to the event and returns true.

 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    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 focusif 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) {// Call OnClickListener performClick(); } } } 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; case MotionEvent.ACTION_DOWN: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (! clickable) { checkForLongClick(0, x, y); break; } if (performButtonActionOnTouchDown(event)) { break; } // 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) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if(! pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback();if((mPrivateFlags & PFLAG_PRESSED) ! = 0) {setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }
Copy the code

Block 3 first determines the clickable | | (viewFlags & TOOLTIP) = = TOOLTIP meet the terms of this event will return true consumption. The following switch mainly deals with the four states of the event respectively. Here’s a quick look at the UP event that calls a performClick method, which calls the onClick method of OnClickListener.

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

Finally, the last line of onTouchEvent returns false by default, meaning that true is returned only if one of the above conditions is met. So far the event distribution of the relevant source code is combed out, I drew a few flow charts, can more clearly understand the source logic.

ViewGroup dispatchTouchEvent logic:

View’s dispathTouchEvent logic

Event distribution overall logic

4. Issues related to event distribution mechanism

After reading the source code, let’s solve the three problems mentioned earlier.

Q1: Why only log DemoACTION_DOWNEvents have a complete run log of distribution intercepts and responses from the Activity to the ViewGroup to the View. WhyACTION_MOVEandACTION_UPThe event didn’t?

A1: Log Demo code all event passing methods are default call super superclass corresponding method, so according to the source logic, when the first DOWN event sequence comes, it will be in the order of Activity–>MyViewGroupA–>MyViewGroupB–>MyView. ViewGroup onInterceptTouchEvent method returns false by default not intercept events, will eventually find the right child View (here the MyView) dispatchTransformedTouchEvent method, We’re going to send the event to the child’s dispatchTouchEvent method, and in dispatchTouchEvent we’re going to call the View’s onTouchEvent method by default. So the default clickable is false, and the onTouchEvent method returns false by default when Clickable is false. Eventually lead to the ViewGroup dispatchTransformedTouchEvent method returns false. MFirstTouchTarget is null, so when subsequent MOVE and UP events come, because mFirstTouchTarget is null, the event interception flag is directly set to true, and the event will not continue to be distributed. The final event is returned to the Activity’s onTouchEvent method without being consumed. So the log output is like this.

 if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! = null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! = 0;if(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); }else {
                    intercepted = false; }}else{//mFirstTouchTarget is empty intercepted istrueThe onInterceptTouchEvent method intercepted = is not calledtrue;
            }
Copy the code
Q2: Why will it be setclickable="true"afterACTION_MOVEandACTION_UPThe event will be executed, right?

A2: If clickable is set to true, the View’s onTouchEvent method will return true. If the DOWN event is consumed, a TouchTarget will be created and inserted into the single linked header. MFirstTouchTarget will not be empty. When MOVE and UP events arrive, they are processed by the View that consumed the DOWN event.

Q3:requestDisallowInterceptTouchEventHow to tell the parent View not to intercept events, and whyonInterceptTouchEventMethod doesn’t execute either?

A3: source reading is see, sign bit by bit operations Settings when requestDisallowInterceptTouchEvent method, after the call to parameter to true, the event will disallowIntercept true when distributing,! DisallowIntercept is false, causing the event intercept flag intercepted to be false and event interception will not occur.

Q4:View.OnClickListenertheonClickThe methods andView.OnTouchListenertheonTouchOrder of execution?

The onClick method of A4: : view. OnClickListener is called in the performClick method of the View’s onTouchEvent. The onTouch method of view. OnTouchListener has a higher priority than the onTouchEvent method in the View dispatchTouchEvent method and as long as onTouchListener. Touch returns true, I’m just going to call OnTouchListener. OnTouch and I’m not going to call onTouchEvent. So the onClick method of view. OnClickListener comes after the onTouch method of view. OnTouchListener.

5. Sliding conflicts

The slippage conflict is explained in detail in The Art of Android Development, and HERE I combine the book’s approach conclusions with specific examples to make a summary.

1. Slide conflict scenarios

  • The direction of external sliding is inconsistent with that of internal sliding
  • The external slide is in the same direction as the internal slide
  • Nesting of the first two cases
2. Handling rules for sliding conflicts

Different scenarios have different processing rules. For example, in scenario 1 above, the general rule is that when sliding left or right, the external View intercepts the event, and when sliding up or down, the internal View intercepts the event. At this time, when dealing with sliding conflicts, we can determine who intercepts the event according to whether the sliding is horizontal or vertical. Scenario And this sliding conflict in the same direction generally depends on the business logic to handle the rules, when external View interception, when internal View interception. Scenario 3 is more complex, but it is also based on the specific business logic to determine the specific sliding rules.

3. Solutions to sliding conflicts
  • External interception
  • Internal interception method

External interception method is to start from the parent View, all events must be distributed and intercepted by the parent View, when the parent View needs events, it will be intercepted, do not need to intercept, by rewriting the parent View onInterceptTouchEvent method to achieve interception rules.

    private int mLastXIntercept;
    private int mLastYIntercept;
    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(meets the interception requirements of the parent container) {intercepted =true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
Copy the code

According to the above pseudocode, according to different interception requirements to modify can solve the sliding conflict.

The idea of internal interception is that the parent View does not intercept the event, and the child View decides to intercept the event. If the child View needs the event, it will consume it directly, and if it does not need it, it will be handed over to the parent View. This method need to match the requestDisallowInterceptTouchEvent method to implement.

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

     switch (event.getAction()) {
     case MotionEvent.ACTION_DOWN: {
         parent.requestDisallowInterceptTouchEvent(true);
         break;
     }
     case MotionEvent.ACTION_MOVE: {
         int deltaX = x - mLastX;
         int deltaY = y - mLastY;
         if(the parent container need such click events) {parent. RequestDisallowInterceptTouchEvent (false);
         }
         break;
     }
     case MotionEvent.ACTION_UP: {
         break;
     }
     default:
         break;
     }
     mLastX = x;
     mLastY = y;
     returnsuper.dispatchTouchEvent(event); Override public Boolean onInterceptTouchEvent(MotionEvent) {int action = event.getAction();if (action == MotionEvent.ACTION_DOWN) {
         return false;
     } else {
         return true; }}Copy the code

If the ACTION_DOWN event is blocked, all subsequent events will not be passed.

4. Sliding conflict instances

Example 1: ScrollView nested with ListView

<? xml version="1.0" encoding="utf-8"? > <cScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/scrollView1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo1Activity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="350dp"
            android:background="#27A3F3"
            android:clickable="true" />

        <ListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:background="#E5F327"
            android:layout_height="300dp"></ListView>

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:background="#0AEC2E"
            android:clickable="true" />
    </LinearLayout>
</cScrollView>
Copy the code

In this case, MyView is just the View that printed the log and didn’t do anything else to hold it up so that the ScrollView can slide beyond one screen. Operation effect:

Start by customizing the ScrollView and rewriting its onInterceptTouchEvent method to block events other than DOWN events.

public class MyScrollView extends ScrollView {

    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

There is no interception of the DOWN event, so the DOWN event cannot enter the onTouchEvent event of the ScrollView, and since the ScrollView needs to do some preparation in the onTouchEvent method, it is called manually. We then define a custom ListView to determine event interception, overriding the dispatchTouchEvent method.

package com.example.sy.eventdemo;

import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListView;

/**
 * Create by SY on 2019/4/22
 */
public class MyListView extends ListView {
    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    private float lastY;

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            if(lastY > ev.gety ()) {// If you want to slide up, you can't slide up any moreif(! canScrollList(1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    return false; }}else if(ev.gety () > lastY) {// If the parent View does not slide down, the parent View does not slide downif(! canScrollList(-1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            }
        }
        lastY = ev.getY();
        returnsuper.dispatchTouchEvent(ev); }}Copy the code

Check whether it’s sliding up or down, whether it’s sliding to the head, if it’s sliding to the head let the parent View intercept the event and let the parent View handle it, otherwise it handles it. Change the space in the layout file.

<? xml version="1.0" encoding="utf-8"? > <com.example.sy.eventdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/scrollView1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo1Activity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="350dp"
            android:background="#27A3F3"
            android:clickable="true" />

        <com.example.sy.eventdemo.MyListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:background="#E5F327"
            android:layout_height="300dp"></com.example.sy.eventdemo.MyListView>

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:background="#0AEC2E"
            android:clickable="true" />
    </LinearLayout>
</com.example.sy.eventdemo.MyScrollView>
Copy the code

Running results:

Example 2: ViewPager nested with ListView


<? xml version="1.0" encoding="utf-8"? > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo2Activity">

    <com.example.sy.eventdemo.MyViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>
</LinearLayout>
Copy the code

The layout of each page in ViewPager is simply a ListView:

<? xml version="1.0" encoding="utf-8"? > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo2Activity">

    <com.example.sy.eventdemo.MyViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>

</LinearLayout>
Copy the code

The initial run without handling sliding collisions looks like this:

    caseMotionEvent.ACTION_MOVE: int gapX = x - lastX; int gapY = y - lastY; // When the horizontal slide distance is greater than the vertical slide distance, let the parent view intercept the eventif (Math.abs(gapX) > Math.abs(gapY)) {
                    intercept = true;
                } else{// Otherwise do not intercept the eventfalse;
                }
                break;
Copy the code

In onInterceptTouchEvent, if the horizontal sliding distance is greater than the vertical sliding distance, the parent view intercepts the event. Otherwise, the parent view does not intercept the event, and the child view processes the event. Running results:

6, summary

  • Android event distribution order: Activity–>ViewGroup–>View
  • Android event response sequence: View–>ViewGroup–>Activity
  • In sliding conflict resolution, the key is to find the interception rule, determine the interception rule according to the operation habits or business logic, and re-correspond the interception method according to the rule.