Public number: byte array, hope to help you 😇😇

View event distribution mechanism has always been a difficult piece of knowledge in Android development. It is very difficult to clarify the rules of MotionEvent flow between ViewGroup and View. The whole process involves distribution, interception and consumption. Each procedure can vary greatly in the process depending on the return value, and the Activity is involved in this process, so it is difficult to understand the distribution rules for touch events without referring to the source code for analysis. In a long time ago I wanted to start writing this piece of knowledge, stay up late liver, I hope to help you 😇😇

I. Coordinate system

The Android coordinate system can be divided into two types: screen coordinate system and View coordinate system

1. Screen coordinate system

The screen coordinate system takes the upper left corner of the screen as the origin of coordinates. The horizontal direction to the right is the positive direction of the X axis, and the vertical direction to the downward is the positive direction of the Y axis

2. View coordinate system

The View coordinate system takes the upper left corner of the ViewGroup where the View is located as the origin of coordinates. The horizontal direction to the right is the positive direction of X axis, and the vertical direction to the downward is the positive direction of Y axis. The View class contains the following methods to get the distance from its parent ViewGroup:

  • GetLeft (). The distance between the left side of the View and the left side of the ViewGroup
  • GetTop (). Distance from the top of the View to the top of the ViewGroup
  • GetRight (). The distance between the right side of the View and the left side of the ViewGroup
  • GetBottom (). The distance from the bottom of the View to the top of the ViewGroup

The View relies on these four distance values to calculate the width and height

	public final int getWidth(a) {
        return mRight - mLeft;
    }

    public final int getHeight(a) {
        return mBottom - mTop;
    }
Copy the code

Second, the MotionEvent

The most common types of touch events are as follows:

  • ACTION_DOWN: The pressing action of the user’s finger. It is the first event triggered when the user touches the screen each time
  • ACTION_MOVE: After the user presses the screen with his finger, if the screen slides beyond a certain threshold before releasing the finger, an ACTION_MOVE event occurs
  • ACTION_UP: Action triggered when the user’s finger leaves the screen. It is the last event of the next touch action

A complete sequence of events contains all the events triggered by the user from pressing down the screen to leaving the screen. In this sequence, there is only one ACTION_DOWN and ACTION_UP, while ACTION_MOVE depends on the situation and the number is greater than or equal to zero (the multi-touch situation is not considered here).

Each event is wrapped as a MotionEvent class

    fun dispatchTouchEvent(event: MotionEvent) {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> TODO()
            MotionEvent.ACTION_MOVE -> TODO()
            MotionEvent.ACTION_UP -> TODO()
        }
    }
Copy the code

MotionEvent contains the coordinate point at which the touch event occurred, divided into two different sets of methods

        // Based on the distance obtained in the upper left corner of View
        motionEvent.getX();
        motionEvent.getY();

        // Based on the distance obtained in the upper left corner of the screen
        motionEvent.getRawX();
        motionEvent.getRawY();
Copy the code

In addition, the system has a built-in minimum sliding distance value, only when the distance between two coordinate points exceeds this value, it will be considered as a sliding event

ViewConfiguration.get(Context).getScaledTouchSlop()	
Copy the code

Three stages of event distribution

Throughout the event distribution process, the two main View types are ViewGroup and View. A complete event distribution process consists of three phases: event publishing, event interception, and event consumption, which correspond to the three methods declared in the View and ViewGroup respectively

1, publish,

Event publishing corresponds to the following method

public boolean dispatchTouchEvent(MotionEvent ev)
Copy the code

This method is used to distribute touch events received by Android views (views, Viewgroups, activities, etc.). If the event can be passed to the current View, this method must be called. That is, all touch events received by the View must be distributed through this method. The return value from this method is used to indicate whether the view or embedded view consumed the event. If the current view type is ViewGroup, the onInterceptTouchEvent(MotionEvent) method is called internally to determine whether to intercept the event

2, intercept,

The interception of events corresponds to the following method

public boolean onInterceptTouchEvent(MotionEvent ev)
Copy the code

The ViewGroup contains this method, but it does not exist in the View. This method returns a value indicating whether the corresponding event needs to be intercepted. Return true to intercept the event, do not continue publishing to the subview, and pass the event to its own onTouchEvent(MotionEvent Event) method for processing; Returning false means that the event is not intercepted and continues to be passed to the subview. If the ViewGroup intercepts an event, the method will not be called again in the same sequence of events

3, consumption

The consumption of events corresponds to the following method

public boolean onTouchEvent(MotionEvent event)
Copy the code

This method returns true to indicate that the current view has processed the corresponding event, and the event will be consumed here, terminating delivery; Returning false means that the current view does not process the event and the event is passed to other views

4. The connection between the three

The ViewGroup contains all three processes, while the View contains only distribution and consumption. The View class does not contain onInterceptTouchEvent(MotionEvent) methods. The connection between the three methods can be expressed in pseudocode as follows:

	fun dispatchTouchEvent(event: MotionEvent): Boolean {
        var consume = false
        consume = if (onInterceptTouchEvent(event)) {
            onTouchEvent(event)
        } else {
            child.dispatchTouchEvent(event)
        }
        return consume
    }
Copy the code

When a touch event occurs, the event distribution process is executed as follows:

  • The root ViewGroup is the first to receive a MotionEvent, and its dispatchTouchEvent method is called. The onInterceptTouchEvent method is called internally to determine whether to intercept the event
  • The onInterceptTouchEvent method of the ViewGroup returns true, indicating that the current ViewGroup must intercept the event, otherwise child (the embedded ViewGroup or View) will be called to repeat the distribution process
  • The View and ViewGroup onTouchEvent methods are used to determine whether to consume the event, and if true is returned, the event has been consumed, terminating delivery

Of course, the event distribution process of a View is not as simple as described above. In fact, the event flow process is very complex, depending on the return value of each method, the direction of the event sequence can be very different. It’s easier to understand by looking directly at the following example

Let me give you an example

1. Print logs

This inherits from RelativeLayout, LinearLayout, and TextView, respectively. Rewrite the above three methods, print the return value of each method, and observe when it is called

Something like this:

class MyRelativeLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {

    protected fun log(any: Any?). {
        Log.e("MyRelativeLayout", any? .toString() ? :"null")}override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> log("dispatchTouchEvent ACTION_DOWN")
            MotionEvent.ACTION_MOVE -> log("dispatchTouchEvent ACTION_MOVE")
            MotionEvent.ACTION_UP -> log("dispatchTouchEvent ACTION_UP")}val flag = super.dispatchTouchEvent(event)
        log("dispatchTouchEvent return: $flag")
        return flag
    }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> log("onInterceptTouchEvent ACTION_DOWN")
            MotionEvent.ACTION_MOVE -> log("onInterceptTouchEvent ACTION_MOVE")
            MotionEvent.ACTION_UP -> log("onInterceptTouchEvent ACTION_UP")}val flag = super.onInterceptTouchEvent(event)
        log("onInterceptTouchEvent return: $flag")
        return flag
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> log("onTouchEvent ACTION_DOWN")
            MotionEvent.ACTION_MOVE -> log("onTouchEvent ACTION_MOVE")
            MotionEvent.ACTION_UP -> log("onTouchEvent ACTION_UP")}val flag = super.onTouchEvent(event)
        log("onTouchEvent return: $flag")
        return flag
    }

}
Copy the code

Nesting levels of layouts:

<github.leavesc.motion_event.MyRelativeLayout>

    <github.leavesc.motion_event.MyLinearLayout>

        <github.leavesc.motion_event.MyTextView/>

    </github.leavesc.motion_event.MyLinearLayout>

</github.leavesc.motion_event.MyRelativeLayout>
Copy the code

Click the TextView content area to print the log information:

MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent return: false

MyLinearLayout: dispatchTouchEvent ACTION_DOWN
MyLinearLayout: onInterceptTouchEvent ACTION_DOWN
MyLinearLayout: onInterceptTouchEvent return: false

MyTextView: dispatchTouchEvent ACTION_DOWN
MyTextView: onTouchEvent ACTION_DOWN
MyTextView: onTouchEvent return: false
MyTextView: dispatchTouchEvent return: false

MyLinearLayout: onTouchEvent ACTION_DOWN
MyLinearLayout: onTouchEvent return: false
MyLinearLayout: dispatchTouchEvent return: false

MyRelativeLayout: onTouchEvent ACTION_DOWN
MyRelativeLayout: onTouchEvent return: false
MyRelativeLayout: dispatchTouchEvent return: false
Copy the code

The following information is obtained from the logs:

  1. When a screen is clicked, the event distribution process starts with the MyRelativeLayout root ViewGroup, even though the currently clicked area is within a TextView. The system will determine which ViewGroup the finger falls in according to the touch point, and then find out which underlying View the coordinate point finally falls in through traversal, and then flow the whole event sequence between the ViewGroup and View
  2. The event distribution flow is passed from the root ViewGroup from top to bottom (from outside in) to the embedded bottom View, from MyRelativeLayout to MyLinearLayout to MyTextView, and then back again from bottom to top (from inside out). The underlying View here refers to the various subclasses of the View class, but also can refer to the View group that does not contain the View. This article specifically refers to various View subclasses of the non-ViewGroup type
  3. For a ViewGroup, onInterceptTouchEvent is called internally from its dispatchTouchEvent method to determine whether interception is needed. If onInterceptTouchEvent returns false, Means that it does not intend to intercept the event, and the child’s dispatchTouchEvent method is called to continue the above steps
  4. If none of the viewgroups embedded in the root ViewGroup intercepts touch events, the events are circulated through the loop to the bottom View
  5. For a View, there is no onInterceptTouchEvent method. The dispatchTouchEvent method calls its onTouchEvent method directly to determine whether to consume the touch event. If false, it means it does not intend to consume the event, and if true, it does consume the event, terminating delivery. Since TextView is unclickable by default, it does not consume any touch events by default. Since no consumer can be found, the events are then returned to the parent container in turn
  6. After MyTextView does not intend to consume the touch event, MyLinearLayout’s onTouchEvent method will then be called, after which MyLinearLayout’s dispatchTouchEvent will finally return the determined value false. The View’s parent, the ViewGroup, is responsible for calling the internal View’s callback event recursively. MyRelativeLayout’s dispatchTouchEvent returns the last value printed

2. Participate in event distribution

As mentioned above, each touch event starts with ACTION_DOWN and ends with ACTION_UP, but the log only sees ACTION_DOWN. Where is ACTION_UP?

In fact, the initial distribution point for touch events should be from the Activity. The Activity receives touch events before the ViewGroup. Rewrite the Activity’s dispatchTouchEvent and onTouchEvent methods. The following logs can be obtained:

MotionMainActivity: dispatchTouchEvent ACTION_DOWN

MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent return: false

MyLinearLayout: dispatchTouchEvent ACTION_DOWN
MyLinearLayout: onInterceptTouchEvent ACTION_DOWN
MyLinearLayout: onInterceptTouchEvent return: false

MyTextView: dispatchTouchEvent ACTION_DOWN
MyTextView: onTouchEvent ACTION_DOWN
MyTextView: onTouchEvent return: false
MyTextView: dispatchTouchEvent return: false

MyLinearLayout: onTouchEvent ACTION_DOWN
MyLinearLayout: onTouchEvent return: false
MyLinearLayout: dispatchTouchEvent return: false

MyRelativeLayout: onTouchEvent ACTION_DOWN
MyRelativeLayout: onTouchEvent return: false
MyRelativeLayout: dispatchTouchEvent return: false

MotionMainActivity: onTouchEvent ACTION_DOWN
MotionMainActivity: onTouchEvent return: false
MotionMainActivity: dispatchTouchEvent return: false
MotionMainActivity: dispatchTouchEvent ACTION_UP
MotionMainActivity: onTouchEvent ACTION_UP
MotionMainActivity: onTouchEvent return: false
MotionMainActivity: dispatchTouchEvent return: false
Copy the code

The following information is obtained from the logs:

  1. The Activity receives touch events before the viewGroups and views do, and ACTION_DOWN events that are not consumed by the ViewGroup and View are eventually consumed by the Activity
  2. Since neither ViewGroup nor View consumes ACTION_DOWN events, subsequent ACTION_UP events will not be sent to them. Instead, the Activity’s onTouchEvent method will be called directly. It’s digested by the Activity

ViewGroup intercepts events

If the ViewGroup itself intercepts and consumes ACTION_DOWN events, that is, onInterceptTouchEvent and onTouchEvent both return true, The dispatchTouchEvent method calls onTouchEvent instead of onInterceptTouchEvent. The dispatchTouchEvent method calls onTouchEvent directly

If the ViewGroup intercepts an ACTION_DOWN event, but the onTouchEvent method does not consume the event, subsequent events in the sequence of events are not received by the ViewGroup and are handled directly by the superview. The same applies to the View’s processing logic for ACTION_DOWN events

If all viewgroups and views do not consume ACTION_DOWN events, subsequent events (ACTION_MOVE, ACTION_UP, etc.) are passed directly to the Activity. Viewgroups and views do not have access to subsequent events

The onInterceptTouchEvent method returns true when it receives an ACTION_DOWN event

class MyLinearLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                log("onInterceptTouchEvent ACTION_DOWN")
                return true
            }
            MotionEvent.ACTION_MOVE -> log("onInterceptTouchEvent ACTION_MOVE")
            MotionEvent.ACTION_UP -> log("onInterceptTouchEvent ACTION_UP")}val flag = super.onInterceptTouchEvent(event)
        log("onInterceptTouchEvent return: $flag")
        return flag
    }

}
Copy the code

At this point MyLinearLayout intercepts the ACTION_DOWN event, so MyTextView will not receive the event. But since MyLinearLayout doesn’t consume the event, the ACTION_DOWN event is still passed back to the parent MyRelativeLayout, which also doesn’t consume the event by default. So subsequent ACTION_UP will only be handled by the Activity

MotionMainActivity: dispatchTouchEvent ACTION_DOWN

MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent return: false

MyLinearLayout: dispatchTouchEvent ACTION_DOWN
MyLinearLayout: onInterceptTouchEvent ACTION_DOWN
MyLinearLayout: onTouchEvent ACTION_DOWN
MyLinearLayout: onTouchEvent return: false
MyLinearLayout: dispatchTouchEvent return: false

MyRelativeLayout: onTouchEvent ACTION_DOWN
MyRelativeLayout: onTouchEvent return: false
MyRelativeLayout: dispatchTouchEvent return: false

MotionMainActivity: onTouchEvent ACTION_DOWN
MotionMainActivity: onTouchEvent return: false
MotionMainActivity: dispatchTouchEvent return: false
MotionMainActivity: dispatchTouchEvent ACTION_UP
MotionMainActivity: onTouchEvent ACTION_UP
MotionMainActivity: onTouchEvent return: false
MotionMainActivity: dispatchTouchEvent return: false
Copy the code

If MyLinearLayout both intercepts and consumes ACTION_DOWN events, it will still receive subsequent events

class MyLinearLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                log("onInterceptTouchEvent ACTION_DOWN")
                return true
            }
            MotionEvent.ACTION_MOVE -> log("onInterceptTouchEvent ACTION_MOVE")
            MotionEvent.ACTION_UP -> log("onInterceptTouchEvent ACTION_UP")}val flag = super.onInterceptTouchEvent(event)
        log("onInterceptTouchEvent return: $flag")
        return flag
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                log("onTouchEvent ACTION_DOWN")
                return true
            }
            MotionEvent.ACTION_MOVE -> log("onTouchEvent ACTION_MOVE")
            MotionEvent.ACTION_UP -> log("onTouchEvent ACTION_UP")}val flag = super.onTouchEvent(event)
        log("onTouchEvent return: $flag")
        return flag
    }

}
Copy the code

MyLinearLayout receives subsequent ACTION_MOVE and ACTION_UP events. Instead of calling onInterceptTouchEvent, MyLinearLayout calls onTouchEvent

MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent return: false

MyLinearLayout: dispatchTouchEvent ACTION_DOWN
MyLinearLayout: onInterceptTouchEvent ACTION_DOWN
MyLinearLayout: onTouchEvent ACTION_DOWN
MyLinearLayout: dispatchTouchEvent return: true

MyRelativeLayout: dispatchTouchEvent return: true
MyRelativeLayout: dispatchTouchEvent ACTION_MOVE
MyRelativeLayout: onInterceptTouchEvent ACTION_MOVE
MyRelativeLayout: onInterceptTouchEvent return: false

MyLinearLayout: dispatchTouchEvent ACTION_MOVE
MyLinearLayout: onTouchEvent ACTION_MOVE
MyLinearLayout: onTouchEvent return: false
MyLinearLayout: dispatchTouchEvent return: false

MyRelativeLayout: dispatchTouchEvent return: false
MyRelativeLayout: dispatchTouchEvent ACTION_UP
MyRelativeLayout: onInterceptTouchEvent ACTION_UP
MyRelativeLayout: onInterceptTouchEvent return: false

MyLinearLayout: dispatchTouchEvent ACTION_UP
MyLinearLayout: onTouchEvent ACTION_UP
MyLinearLayout: onTouchEvent return: false
MyLinearLayout: dispatchTouchEvent return: false

MyRelativeLayout: dispatchTouchEvent return: false
Copy the code

Another important point to note is that even if each ACTION_MOVE event is not consumed, The MyLinearLayout will receive the entire sequence of events and the onTouchEvent method of the parent container will not be called back. Normally, a sequence of events should only be handled by a single View or ViewGroup. Since MyLinearLayout already consumes ACTION_DOWN events, it should also handle subsequent events

4. View consumption event

The View does not intercept events, but if ACTION_DOWN events are consumed, subsequent events will be received

class MyTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                log("onTouchEvent ACTION_DOWN")
                return true
            }
            MotionEvent.ACTION_MOVE -> log("onTouchEvent ACTION_MOVE")
            MotionEvent.ACTION_UP -> log("onTouchEvent ACTION_UP")}val flag = super.onTouchEvent(event)
        log("onTouchEvent return: $flag")
        return flag
    }

}
Copy the code

As you can see, MyTextView receives subsequent ACTION_MOVE and ACTION_UP events

MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
MyRelativeLayout: onInterceptTouchEvent return: false

MyLinearLayout: dispatchTouchEvent ACTION_DOWN
MyLinearLayout: onInterceptTouchEvent ACTION_DOWN
MyLinearLayout: onInterceptTouchEvent return: false

MyTextView: dispatchTouchEvent ACTION_DOWN
MyTextView: onTouchEvent ACTION_DOWN
MyTextView: dispatchTouchEvent return: true

MyLinearLayout: dispatchTouchEvent return: true

MyRelativeLayout: dispatchTouchEvent return: true
MyRelativeLayout: dispatchTouchEvent ACTION_MOVE
MyRelativeLayout: onInterceptTouchEvent ACTION_MOVE
MyRelativeLayout: onInterceptTouchEvent return: false

MyLinearLayout: dispatchTouchEvent ACTION_MOVE
MyLinearLayout: onInterceptTouchEvent ACTION_MOVE
MyLinearLayout: onInterceptTouchEvent return: false

MyTextView: dispatchTouchEvent ACTION_MOVE
MyTextView: onTouchEvent ACTION_MOVE
MyTextView: onTouchEvent return: false
MyTextView: dispatchTouchEvent return: false

MyLinearLayout: dispatchTouchEvent return: false

MyRelativeLayout: dispatchTouchEvent return: false
MyRelativeLayout: dispatchTouchEvent ACTION_UP
MyRelativeLayout: onInterceptTouchEvent ACTION_UP
MyRelativeLayout: onInterceptTouchEvent return: false

MyLinearLayout: dispatchTouchEvent ACTION_UP
MyLinearLayout: onInterceptTouchEvent ACTION_UP
MyLinearLayout: onInterceptTouchEvent return: false

MyTextView: dispatchTouchEvent ACTION_UP
MyTextView: onTouchEvent ACTION_UP
MyTextView: onTouchEvent return: false
MyTextView: dispatchTouchEvent return: false

MyLinearLayout: dispatchTouchEvent return: false

MyRelativeLayout: dispatchTouchEvent return: false
Copy the code

Same as in the previous example. Even if each ACTION_MOVE event is not consumed by MyTextView, MyTextView can still receive the entire event sequence, and the parent container’s onTouchEvent method will not be called back. The whole sequence of events is just handled by MyTextView

We could also modify the code to make the upper ViewGroup actively intercept subsequent events, but this would cause some problems because if MyTextView does not receive an ACTION_UP event, its OnClickListener cannot be called back

In general, whether a View can receive the message of the entire event sequence depends on whether it consumes the ACTION_DOWN event, which is the starting point of the entire event sequence. The View must consume the initial event before it has a chance to process the entire event sequence

Five, the summary

  1. An Activity receives touch events before any ViewGroup or View receives them. An Activity can prevent the ViewGroup or View from receiving any event by actively intercepting the delivery of each event. If the ViewGroup or View receives an ACTION_DOWN event but does not consume it, the event will eventually be consumed by the Activity
  2. When the touch event is triggered, the system will find the root ViewGroup according to the coordinate system of the touch point, and then send the event to the underlying View. That is, the event distribution process is first transmitted from the root ViewGroup from top to bottom (from outside to inside) to the embedded underlying View. If the event is not consumed during this process, And then eventually it goes the other way — from the bottom up
  3. When a ViewGroup receives an ACTION_DOWN event, its dispatchTouchEvent method calls onInterceptTouchEvent to determine whether to intercept it. If the onInterceptTouchEvent method returns false, which means it does not intend to intercept the event, child’s dispatchTouchEvent method is called and the above steps continue. If intercepted, onTouchEvent is called for consumption
  4. If the ViewGroup itself intercepts and consumes ACTION_DOWN events, subsequent events in the sequence of events will be handled by the ViewGroup (if they can be received at all) without calling its onInterceptTouchEvent method. The Child is not iterated again, and the dispatchTouchEvent method calls the onTouchEvent method directly. This is to avoid invalid operations and improve the rendering efficiency of the system
  5. If neither the root ViewGroup nor any embedded ViewGroup intercepts an ACTION_DOWN event, the event is circulated through a loop to the lowest level View. For a View, there is no onInterceptTouchEvent method. The dispatchTouchEvent method calls its onTouchEvent method to decide whether to consume the event. If false is returned, it means that it does not intend to consume the event, which in turn calls the parent’s onTouchEvent method; Returning true means the event is consumed and the event terminates
  6. Regardless of whether the ViewGroup intercepts ACTION_DOWN events or not, as long as the ViewGroup itself and all children do not consume ACTION_DOWN events, the dispatchTouchEvent method returns false, The ViewGroup will not receive subsequent events, which will be digested directly by the Activity
  7. A ViewGroup or View that consumes ACTION_DOWN events can receive the entire sequence of events, even if the onTouchEvent method returns false for each subsequent event. Subsequent events are delivered from the top down based on the chain of references reserved in the processing of ACTION_DOWN events
  8. Whether the View can receive the message of the entire event sequence depends on whether it consumes the ACTION_DOWN event, which is the starting point of the entire event sequence. The View must consume the initial event to have the chance to process the entire event sequence
  9. The upstream ViewGroup does not care which downstream ViewGroup or View consumes the touch event. As long as the downstream dispatchTouchEvent method returns true, the upstream continues to send subsequent events to the downstream
  10. ViewGroup and View are independent of each event sequence consumption process, that is, the consumption result of the last event sequence does not affect the new event sequence

Sixth, the View

View is one of the most basic basic classes of the Android system, here to do the analysis of the View event distribution source code, in order to verify the conclusion I give above, based on the SDK 30 version analysis

dispatchTouchEvent

The View dispatchTouchEvent method is logically simple and can be summed up as:

  1. Correspond to step 1. If the View is ENABLED and available, and the ScrollBarDragging event output by the mouse device has been processed, then the current View has consumed the touch event
  2. Corresponding to step 2. If the View is ENABLED and the external OnTouchListener returns true, then the View is sent to the external to consume the touch event
  3. Corresponding to step three. If none of the above steps is true, the onTouchEvent method is called again. If this method returns true, then the current View consumed the touch event
  4. So, an externally set OnTouchListener takes precedence over its own onTouchEvent method. OnTouchListener can prevent the onTouchEvent method from being called by returning true
    public boolean dispatchTouchEvent(MotionEvent event) {...// Is used to indicate whether the current View consumes the event
        boolean result = false;
        final intactionMasked = event.getActionMasked(); ...if (onFilterTouchEventForSecurity(event)) {
            / / the first step
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            / / the second step
            ListenerInfo li = mListenerInfo;
            if(li ! =null&& li.mOnTouchListener ! =null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

           	/ / the third step
            if(! result && onTouchEvent(event)) { result =true; }}...return result;
    }
Copy the code

onTouchEvent

The onTouchEvent method is a bit more complicated. We can just look at the main idea, which can be summarized as:

  1. Correspond to step 1. If the current View is DISABLED, clickable determines whether the current View consumes touch events. Return true if at least one of the CLICKABLE, LONG_CLICKABLE, and CONTEXT_CLICKABLE conditions is met. These three conditions correspond to: clickable, clickable and context-clickable
  2. Corresponding to step 2. Return true if the TouchDelegate exists and consumes the touch event
  3. Corresponding to step three. If the current View is clickable or(viewFlags & TOOLTIP) == TOOLTIPIf true, it also consumes the current event. TOOLTIP can be addedandroid:tooltipText="tips"After opening TextView, long press TextView to display a prompt text in the form of a floating window
  4. Correspond to step 4. Determines whether to call back the external OnClickListener when the ACTION_UP event is received. So if the external OnTouchListener returns true, then OnClickListener has no chance to be called at all, and if the upper View consumes an ACTION_UP event or the current View is DISABLED, OnClickListener is also not called
    public boolean onTouchEvent(MotionEvent event) {...final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if((viewFlags & ENABLED_MASK) == DISABLED) {···/ / the first step
            return clickable;
        }
        / / the second step
        if(mTouchDelegate ! =null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true; }}/ / the third step
        if(clickable | | (viewFlags & TOOLTIP) = = TOOLTIP) {...if(! focusTaken) {if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if(! post(mPerformClick)) {/ / step 4performClickInternal(); }}...return true;
        }
        return false;
    }
Copy the code

So, the onTouchEvent method is actually called inside dispatchTouchEvent, and if the View is clickable, then the touch event is consumed, and the OnClickListener is called inside the onTouchEvent method

Let me give you an example. TextView is unclickable by default, while Button is directly inherited from TextView, so the default state of Button is also unclickable and does not consume any touch events. The reason why Button consumes touch events in our daily use is that The Button’s Clickable is set to true when OnClickListener is set

    public void setOnClickListener(@Nullable OnClickListener l) {
        if(! isClickable()) { setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
Copy the code

Seven, ViewGroup

ViewGroup directly inherits from View, and its logic is expanded on the basis of View. Here we directly see how the ViewGroup class implements the three methods described above

dispatchTouchEvent

The dispatchTouchEvent method for a ViewGroup is a lot more complicated than a View, because a View is at the bottom of the hierarchy and you just need to manage itself, whereas a ViewGroup needs to manage its embedded layout, There may be multiple child ViewGroups and child views

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {...boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            / / the first step
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            / / the second step
            final boolean intercepted;
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
                final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
                if(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); }else {
                    intercepted = false; }}else {
                intercepted = true; }...if(! canceled && ! intercepted) { View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() :null;

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

                    / / the third step
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null&& childrenCount ! =0) {...final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; I --) {···}} ···} ···}}if (mFirstTouchTarget == null) {
                / / step 4
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                / / step 5
                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; ...}}}if(! handled && mInputEventConsistencyVerifier ! =null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
Copy the code

The main process of this method can be summarized as follows:

  1. Correspond to step 1. If an ACTION_DOWN event is currently received, indicating a new sequence of events, the mFirstTouchTarget reference is cleared. In each sequence of events, if the Child consumes an ACTION_DOWN event, the ViewGroup is referred to the Child via mFirstTouchTarget, and subsequent events can be passed directly through that reference without needing to iterate again
  2. Corresponding to step 2. This step is used to determine whether to intercept the event. If the if condition is true, the current processing isNew sequence of eventsOr is itThe event after ACTION_DOWN and the previous ACTION_DOWN has been consumed by the childThe onInterceptTouchEvent method is called to determine whether to intercept. And if the child actively passes the callmParent.requestDisallowInterceptTouchEventIf the current ViewGroup is requested not to intercept (even if disallowIntercept is true), set intercepted to false and do not intercept. This means that, unless the Child asks the ViewGroup not to intercept, the parent layout of the event sequence belonging to the Child still has a chance to intercept
  3. In step 2, note that ACTION_DOWN events are not controlled by FLAG_DISALLOW_INTERCEPT, that is, child cannot actively prevent ViewGroup from not intercepting ACTION_DOWN events, The onInterceptTouchEvent method of the ViewGroup is still called
  4. In the second step, assuming that the ViewGroup intercepts ACTION_DOWN, the mFirstTouchTarget is not assigned, which causes the if statement to fail on receiving subsequent events. The onInterceptTouchEvent method will be executed only once in the entire event sequence, which is one of the summarizations given above
  5. Corresponding to step three. The ACTION_DOWN event will go here, and since intercepted is currently false, that is, it will not intercept the event, so it will go through the children to determine which child the touch point coordinate system falls in, If found, point to the child with mFirstTouchTarget
  6. Correspond to step 4. If mFirstTouchTarget is null, the ViewGroup does not find the next child that can receive the event. Either there is no child, neither child handles the event, or the ViewGroup intercepts the event itself. So will the current View subclasses of ViewGroup as an ordinary by calling dispatchTransformedTouchEvent method to perform the parent View dispatchTouchEvent method, Follow the original View distribution logic. So the ViewGroup will call onTouchEvent after actively intercepting the event
  7. In step 4, if the ViewGroup finally consumes the event, then when the subsequent event is received, mFirstTouchTarget does not point to Child and is still null, so the else statement in step 2 will be used instead of traversing children. This means that subsequent events will neither call back the onInterceptTouchEvent method nor traverse the Child, which is one of the things I summarized above
  8. Correspond to step 5. If mFirstTouchTarget is not null, then child’s dispatchTouchEvent method is called and the above steps are repeated to know that the child handled the event
  9. So the ViewGroup, through this recursive call, will eventually return the final event processing result for the upper-level view Activity

onInterceptTouchEvent

The onInterceptTouchEvent method only returns true in certain cases, which seems to be true only if there is an external mouse device. Remember that this method returns false by default, and it does not intercept by default

    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

onTouchEvent

ViewGroup does not override its parent View’s onTouchEvent method, so this method is consistent with View logic

Activity, PhoneWindow, and DecorView

In many places, the Activity participates in the event distribution mechanism of the View. In fact, the process includes PhoneWindow and DecorView as well as the Activity, but both are included inside the Activity. This is something you don’t normally touch in your daily development. So here’s how these three things work

  • Each Activity corresponds to a PhoneWindow, that is, each Activity instance contains a PhoneWindow instance
  • Each PhoneWindow corresponds to a DecorView that relies on the PhoneWindow as one of its construction parameters to instantiate
  • DecorView is a subclass of FrameLayout and is the root View of the Activity tree. The View added by calling setContentView corresponds to the ContentParent area of the DecorView
  • Of the three, the DecorView receives the touch event first, and as the root View of the View tree, the DecorView is responsible for sending the touch event to its internal View

DecorView’s dispatchTouchEvent method takes the window. Callback object inside the PhoneWindow and forwards the event to it, where window. Callback actually corresponds to an Activity. The Activity implements the window. Callback interface

public class DecorView extends FrameLayout implements RootViewSurfaceTaker.WindowCallbacks {
    
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        returncb ! =null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {...}@Override
    public boolean onTouchEvent(MotionEvent event) {
        returnonInterceptTouchEvent(event); }}Copy the code

The logic of the Activity’s dispatchTouchEvent and onTouchEvent methods is relatively simple. When receiving an event, PhoneWindow will determine whether to consume the event. If so, it will directly submit the event to PhoneWindow. In addition, the Activity also provides an empty implementation of the onUserInteraction method, which provides subclasses with notification of the ACTION_DOWN event being triggered

public class Activity {
    
    private Window mWindow;
    
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
    public void onUserInteraction(a) {}public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false; }}Copy the code

PhoneWindow is the only implementation of the abstract class Window, which in turn passes the corresponding event to the DecorView

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    
    private DecorView mDecor;
    
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        returnmDecor.superDispatchTouchEvent(event); }}Copy the code

Go back to the DecorView. The superDispatchTouchEvent method of the DecorView directly calls the dispatchTouchEvent method of the parent class

public class DecorView extends FrameLayout implements RootViewSurfaceTaker.WindowCallbacks {
    
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        returncb ! =null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {...}@Override
    public boolean onTouchEvent(MotionEvent event) {
        returnonInterceptTouchEvent(event); }}Copy the code

What is the connection between the three? The DecorView passes the event to the Activity, which passes the event to the PhoneWindow, which passes the event to the DecorView, The DecorView ends up dispatching events the way the ViewGroup defaults to, which looks like it’s going in circles. What’s the point of this design?

In fact, the DecorView, as the first recipient of touch events, acts as a bridge between the touch events delivered from the system to the Activity, which the developer can directly touch and inherit from. The DecorView needs to forward the event to the outermost Activity first, allowing the developer to intercept the current screen touch event by overriding the dispatchTouchEvent and onTouchEvent methods. The DecorView, as the root node of the View tree, receives the event from the PhoneWindow and distributes the event to the child views, thus tying the chain of events together

Therefore, the event flow mechanism between these three can be said to give the developer an opportunity for global event interception

Nine, sliding conflict

If both the parent container and the child View can respond to a sliding event, then a sliding conflict can occur. There are two methods to solve sliding conflict: external interception and internal interception

1. External interception

The parent container selectively intercepts touch events in the onInterceptTouchEvent method according to the actual situation. If it determines that the current sliding event is needed, it intercepts the event and consumes it; otherwise, it is processed by the child View. There are a few caveats to this approach:

  • ACTION_DOWN events cannot be intercepted by the parent container. Otherwise, according to the event distribution mechanism of View, subsequent ACTION_MOVE and ACTION_UP events will be processed by the parent container by default
  • According to the actual business requirements, the parent container determines whether the ACTION_MOVE event needs to be processed. If it needs to be processed, the parent container will intercept the consumption; otherwise, the child View will process the ACTION_MOVE event
  • In principle, the parent ACTION_UP event should not be intercepted, otherwise the child View’s onClick event will not be triggered

Pseudo code:

override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
    var intercepted = false
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            intercepted = false
        }
        MotionEvent.ACTION_MOVE -> {
            intercepted = if(Meet interception requirements) {true
            } else {
                false
            }
        }
        MotionEvent.ACTION_UP -> {
            intercepted = false}}return intercepted
}
Copy the code

2. Internal interception

The internal interception rule requires the parent container not to intercept any events, and all events are passed to the child View, which decides whether to consume them or pass them back to the parent container for processing. There are a few caveats to this approach:

  • The parent container cannot intercept ACTION_DOWN events, otherwise subsequent touch events will not be received by child views
  • The slide event rounding logic is placed in the child ViewdispatchTouchEventMethod is called if the parent container needs to handle eventsparent.requestDisallowInterceptTouchEvent(false)Method tells the parent container to intercept the event

Pseudo code:

The child View modifies its dispatchTouchEvent method to control whether the parent container is allowed to intercept events as required

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            // Let the parent container not intercept subsequent ACTION_DOWN events
            parent.requestDisallowInterceptTouchEvent(true)
        }
        MotionEvent.ACTION_MOVE -> {
            if(Parent container requires this event) {// Make the parent container intercept subsequent events
                parent.requestDisallowInterceptTouchEvent(false)
            }
        }
        MotionEvent.ACTION_UP -> {
        }
    }
    return super.dispatchTouchEvent(event)
}
Copy the code

Since the ViewGroup dispatchTouchEvent method preempts whether the child View has asked it not to intercept events, it will call its own onInterceptTouchEvent method if it does not, so in addition to ACTION_DOWN, If the subview doesn’t intercept then the ViewGroup does

override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
    returnevent.action ! = MotionEvent.ACTION_DOWN }Copy the code

Resolving sliding conflicts

I often see some developers on the Internet asking how to solve the problem of ScrollView nesting ScrollView internal ScrollView can not slide, this problem is because of the sliding conflict, The root cause is that the internal ScrollView has been unable to respond to the sliding events because the user’s sliding operations are intercepted and consumed by the external ScrollView. Here is a ScrollView nested ScrollView case as an example, to see how to solve the sliding conflict between them

The layout of the page is shown below. The internal ScrollView cannot slide alone, but can only slide up and down with the external ScrollView


      
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="200dp">

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

                <include layout="@layout/item_content_b" />

                <include layout="@layout/item_content_b" />

                <include layout="@layout/item_content_b" />

                <include layout="@layout/item_content_b" />

                <include layout="@layout/item_content_b" />

                <include layout="@layout/item_content_b" />

            </LinearLayout>

        </ScrollView>

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

        <include layout="@layout/item_content_a" />

    </LinearLayout>

</ScrollView>
Copy the code

Internal interception is chosen to solve the problem. The first step is to have the external ScrollView intercept any event outside ACTION_DOWN

class ExternalScrollView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ScrollView(context, attrs, defStyleAttr) {

    override fun onInterceptTouchEvent(motionEvent: MotionEvent): Boolean {
        val intercepted: Boolean
        when (motionEvent.action) {
            MotionEvent.ACTION_DOWN -> {
                intercepted = false
                super.onInterceptTouchEvent(motionEvent)
            }
            else -> {
                intercepted = true}}return intercepted
    }

}
Copy the code

The internal ScrollView determines whether it is still slideable. If it slides to the top and wants to slide down again, or if it slides to the bottom and wants to slide up again, the external ScrollView will process all the events. In other cases, it will directly intercept and consume the events. So the internal ScrollView can slide inside

class InsideScrollView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ScrollView(context, attrs, defStyleAttr) {

    private var lastX = 0f

    private var lastY = 0f

    override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
        val x = motionEvent.x
        val y = motionEvent.y
        when (motionEvent.action) {
            MotionEvent.ACTION_DOWN -> {
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = x - lastX
                val deltaY = y - lastY
                if (abs(deltaX) < abs(deltaY)) { // Slide up and down
                    if (deltaY > 0) { // Slide down
                        if (scrollY == 0) { // Slide to the top
                            parent.requestDisallowInterceptTouchEvent(false)}}else { // Slide up
                        if (height + scrollY >= computeVerticalScrollRange()) { // Slide to the bottom
                            parent.requestDisallowInterceptTouchEvent(false)
                        }
                    }
                }
            }
            MotionEvent.ACTION_UP -> {
            }
        }
        lastX = x
        lastY = y
        return super.dispatchTouchEvent(motionEvent)
    }

}
Copy the code

11. Q&a session

1. Why is the event from the outside in?

In the example provided above, when the MyTextView field is clicked, the outermost MyRelativeLayout will still receive the first touch event. So why does Android design event distribution to be outside-in? Can it be an inside-out form? Or do you just hand it over to the View where the click area is?

It is definitely not possible to handle touch events only in the View where the click area is located. Imagine a situation where a ViewPager contains multiple fragments, and each Fragment contains a RecyclerView. If the touch events are handled only by RecyclerView, So RecyclerView can normally respond to sliding up and down events, but ViewPager can not slide left and right, because sliding left and right events are consumed by RecyclerView, even if the event is not needed for RecyclerView itself. So the event distribution mechanism must include a flow of touch events between the parent container and the child content area, with each View selectively consuming as needed

Can ** be in the form of the inside out? It’s not appropriate. ** A ViewGroup may contain one or more views, and the ViewGroup needs to determine the next recipient of the touch event by determining which View region the coordinate system of the touch point is located in. And we know that touch events are passed from the outside in order: DecorView -> Activity -> PhoneWindow -> DecorView -> ContentView, since early recipients of touch events are already in the outer DecorView, Therefore, it is more appropriate to transmit from the outside in (this is just my personal opinion, please point out if you are wrong).

2, How to design mFirstTouchTarget?

Since the occurrence frequency of touch events is very high and the nesting level of the layout may be very deep, it is not conducive to improve the drawing efficiency if the full traversal is carried out every time the event is delivered. To improve event delivery efficiency and reduce repeated object creation, a global variable of type TouchTarget, mFirstTouchTarget, is declared in ViewGroup

The child variable in mFirstTouchTarget points to the downstream View that consumes the touch event, and each level of ViewGroup points downstream through the mFirstTouchTarget, so that when a subsequent event arrives, There is no need to iterate through the DFS algorithm again, passing the subsequent event layers down to the final consumer via mFirstTouchTarget

In addition, the static member variable sRecycleBin in TouchTarget is used to reuse objects. It can cache MAX_RECYCLED objects as a linked list. When the obtain method is called, a separate TouchTarget object is obtained in the form of a toggle next reference

	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.
        @UnsupportedAppUsage
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        public TouchTarget next;

        @UnsupportedAppUsage
        private TouchTarget(a) {}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(a) {
            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

3. What about the ACTION_CANCEL event?

Normally, each sequence of events should be consumed by only one View or ViewGroup, but there is a special case where the View consumes ACTION_DOWN events. Subsequent ACTION_MOVE and ACTION_UP events are intercepted by its upper container ViewGroup, so the View cannot receive subsequent events. This can cause some exceptions, such as the Button being pressed after receiving the ACTION_DOWN event, and may not be able to restore the UI state if it does not receive the ACTION_UP end event. To solve this problem, the Android system uses the ACTION_CANCEL event as another end-of-event message

When situations appeal, ViewGroup will through dispatchTransformedTouchEvent method to construct a ACTION_CANCEL event and will be sent out under the View, This allows the View to know that the sequence of events has ended even if it does not receive an ACTION_UP event

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

        // Canceling motions is a special case. We don't need to perform any transformations
        // or filtering. The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            returnhandled; }...}Copy the code

At the same time, the ViewGroup removes the View from the mFirstTouchTarget so that subsequent events will not attempt to deliver to the View

4, What is the effect of onUserInteraction?

The Activity provides an empty implementation of the onUserInteraction method, which provides notification of ACTION_DOWN events to subclasses. What can this method do?

The onUserInteraction method is called only when the Activity receives an ACTION_DOWN event, which can be used for requirements that need to know if the Activity is in a long-term “idle” state. For example, if we want to hide the title bar automatically after the Activity has not been active for a while, we can use this method to set a scheduled task to control the hidden state of the title bar

12. Demo Download

I’ve put all of the above sample code on Github for on-demand use: AndroidOpenSourceDemo