This article will take you to learn in-depth about the distribution of touch events. The specific content is as follows
1. Touch action and event sequence
(1) Action of touch event
There are three types of touch actions: ACTION_DOWN, ACTION_MOVE, ACTION_UP. When the user's finger touches the screen, a touch event as ACTION_DOWN is generated. At this time, if the user's finger immediately leaves the screen, a touch event as ACTION_UP will be generated; if the user's finger continues to slide after touching the screen, when the sliding distance exceeds the predefined distance constant in the system, a touch event as ACTION_MOVE is generated. The predefined distance constant used in the system to determine whether the user's finger's sliding on the screen is an ACTION_MOVE action is called TouchSlop, which can be obtained through (getContext()).getScaledTouchSlop().
(2) Event sequence
When the user's finger touches the screen, slides on the screen, and leaves the screen, this process will produce a series of touch events: ACTION_DOWN--> Several ACTION_MOVE-->ACTION_UP. This series of touch events is a sequence of events.
2. Distribution of touch events
(1) Overview
When a touch time is generated, the system is responsible for handling this touch event to a View (TargetView) and the process of passing the touch event to the TargetView is the distribution of the touch event.
The order of distribution of touch events: Activity-->Top View-->SubView of top view-->. . .-->Target View
Response order of touch events: TargetView --> TargetView's parent container --> . . --> Top View --> Activity
(2) The specific process of toush event distribution
a. Activity's distribution of touch events
When the user touches the screen with his finger, a touch event is generated. The MotionEvent encapsulating the touch event is first passed to the current Activity. The dispatchTouchEvent method of the Activity is responsible for the distribution of the touch event. The actual work of distributing touch events is done by the Window of the current Activity, and the Window will pass the touch event to the DecorView (the current user interface top-level View). The dispatchTouchEvent method code of Activity is as follows:
public boolean dispatchTouchEvent(MotionEvent ev) { if (() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
According to the above code, you can know that the touch event will be handed over to the Window's superDispatchTouchEvent for distribution. If this method returns true, it means that the distribution process of the touch event is over. Returning false means that after layer-by-layer distribution, no subView handles this event, that is, the onTouchEvent method of all subViews returns false (that is, this touch event is not "consumed"). At this time, the onTouchEvent method of the Activity will be called to handle the touch event.
In the Window's superDispatchTouchEvent method, the touch event will first be distributed to the DecorView because it is the top view of the current user interface. The superDispatchTouchEvent method of Window is as follows:
public abstract boolean superDispatchTouchEvent(MotionEvent ev);
It is an abstract method. This method is implemented by Window's implementation class PhoneWindow. The code of PhoneWindow's superDispatchTouchEvent method is as follows:
public boolean superDispatchTouchEvent(MotionEvent ev) { return (event); }
From the above code, the superDispatchTouchEvent method of PhoneWindow actually completes its work through the superDispatchTouchEvent method of DecorView. That is to say, the current Window of Activity directly passes this touch event to DecorView. In other words, the touch event has been distributed as follows: Activity-->Window-->DecorView.
b. Top View distribution of touch events
After the distribution of Activity and Window, the touch event has now been passed to the dispatchTouchEvent method of DecorView. DecorView is essentially a ViewGroup (more specifically FrameLayout). The work done by the dispatchTouchEvent method of ViewGroup can be divided into the following stages. The main code of the first stage is as follows:
//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(); }
There are two main tasks in the first stage: one is to reset the FLAG_DISALLOW_INTERCEPT tag in the resetTouchState method in line 6; the other is to clear the touch target of the current MotionEvent. Regarding the FLAG_DISALLOW_INTERCEPT tag and touch target, there will be relevant explanations below.
The main work of the second phase is to decide whether the current ViewGroup intercepts the touch event. The main code is as follows:
//Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWM || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); (action); //restore action in case it was changed } else { intercepted = false; } } else { //There are no touch targets and this action is not an initial down so this view group continues to intercept touches. intercept =true; }
From the above code, we can know that when a touch event is passed to the ViewGroup, we will first determine whether the action of the touch event is ACTION_DOWN. If this event is ACTION_DOWN or mFirstTouchTarget is not null, we will decide whether to intercept the touch event based on the FLAG_DISALLOW_INTERCEPT flag. So what is mFirstTouchTarget? When the touch event is successfully processed by the child View of ViewGroup, mFirstTouchTarget will be assigned as the View that successfully handles the touch event, which is the touch target increased above.
Summarize the process of the above code: When the subView does not interfere with the interception of ViewGroup (disallowIntercept in the above code is false), if the current event is ACTION_DOWN or mFirstTouchTarget is not empty, the ViewGroup's onInterceptTouchEvent method will be called to determine whether to intercept the event in the end; otherwise (there is no TargetView and this event is not ACTION_DOWN), the current ViewGroup will intercept this event. Once the ViewGroup intercepts a touch event, the mFirstTouchTarget will not be assigned. Therefore, when ACTION_MOVE or ACTION_UP is passed to the ViewGroup, the mTouchTarget is null, so the condition in line 3 of the above code is false, and the ViewGroup will intercept it. The conclusion that can be drawn is that once the ViewGroup intercepts an event, the remaining events in the same event sequence will also be intercepted by default without asking whether to intercept (that is, onInterceptTouchEvent will not be called again).
There is a special case here, that is, the child View sets the FLAG_DISALLOW_INTERCEPT of the parent container to true through the requestDisallowInterceptTouchEvent method. This flag indicates whether the parent container is not allowed to intercept, and if true means it is not allowed. Doing so can prevent the parent container from intercepting all touch events except ACTION_DOWN. The reason why the ACTION_DOWN event cannot be intercepted is that whenever the ACTION_DOWN event arrives, the FLAG_DISALLOW_INTERCEPT flag is reset to the default value (false). Therefore, whenever a new touch event sequence is started (that is, an ACTION_DOWN action is arrived), the ViewGroup will be asked whether to intercept this event by calling onInterceptTouchEven. When the ACTION_DOWN event arrives, the work of resetting the mark bit is done in the first phase above.
Next, we will enter the third stage of work:
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { // Not ACTION_CANCEL and not intercepted if (actionMasked == MotionEvent.ACTION_DOWN) { // If the current event is ACTION_DOWN, go to find the new touch target for this event final int actionIndex = (); // always 0 for down ... final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { // Find a touch target that can receive this event based on the coordinates of the touch final float x = (actionIndex); final float y = (actionIndex); final View[] children = mChildren; // traverse all child Views for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = i; final View child = children[childIndex]; // Find subViews that can receive this event and the coordinates of the touch event are within its area if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } newTouchTarget = getTouchTarget(child); // A subView that meets the criteria was found and assigned to newTouchTarget if (newTouchTarget != null) { //Child is already receiving touch within its bounds. //Give it the new pointer in addition to ones it is handling. |= idBitsToAssign; break; } resetCancelNextUpFlag(child); // Pass the ACTION_DOWN event to the child component for processing if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //Child wants to receive touch within its bounds. mLastTouchDownTime = (); if (preorderedList != null) { //childIndex points into presorted list, find original index for (int j=0;j<childrenCount;j++) { if (children[childIndex]==mChildren[j]) { mLastTouchDownIndex=j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = (); mLastTouchDownY = (); // Assign mFirstTouchTarget to newTouchTarget, this child View becomes the starting point of a new touch event newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } } }
When the ViewGroup does not intercept this event, the touch event will be distributed to its subView for processing. The relevant code starts from line 21: iterate through all subViews of ViewGroup to find a subView that can handle this touch event. If a subView is not playing an animation and the coordinates of the touch event are in its area, the subView can handle this touch event and will assign the subView to newTouchTarget.
If the currently traversed subView can handle this touch event, it will enter the dispatchTransformedTouchEvent method in line 38, which actually calls the dispatchTouchEvent method of the subView. The relevant code in the dispatchTransformedTouchEvent method is as follows:
if (child == null) { handled = (event); } else { handled = (event); }
If the child parameter passed in the dispatchTransformedTouchEvent method is not null, the dispatchTouchEvent method of child (that is, the childView that handles the touch event) will be called. If the dispatchTouchEvent method of the subView returns true, the dispatchTransformedTouchEvent method will also return true, which means that a touch target that handles the event has been successfully found, and the newTouchTarget will be assigned to mFirstTouchTarget on line 55 (this assignment process is completed inside the addTouchTarget method), and the loop of traversing the subView will be jumped out. If the dispatchTouchEvent method of the child View returns false, the ViewGroup will distribute the event to the next child View.
If the touch event is not processed after traversing all child Views (the ViewGroup does not have child Views or the dispatchTouchEvent of all child Views returns false), the ViewGroup will handle the touch event itself. The relevant code is as follows:
if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }
From the above code, we can see that when ViewGroup handles touch events by itself, it will call the dispatchTransformedTouchEvent method, and the child parameter passed is null. According to the above analysis, when the passed chid is null, the method will be called, that is, the dispatchTouchEvent method of the View class is called.
c. View handles touch events
The main code of View's dispatchTouchEvent method is as follows:
public boolean dispatchTouchEvent(MotionEvent event) { boolean result = false; . . . if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && != null && (mViewFlags & ENABLED_MASK) == ENABLED && (this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } . . . return result; }
From the above code, we can see that the View handles the touch event as follows: Since the View does not contain child elements, it can only handle the event itself. It first determines whether the OnTouchListener is set. If it is set, the onTouch method will be called. If the onTouch method returns true (indicating that the touch event has been consumed), the onTouchEvent method will not be called again; if the onTouch method returns false or the OnTouchListener is not set, the onTouchEvent method will be called. The relevant code for onTouchEvent to handle the touch event is as follows:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { . . . if (!mHasPerformedLongPress) { //This is a tap, so remove the longpress check removeLongPressCallback(); //Only perform take click actions if we were in the pressed state if (!focusTaken) { //Use a Runnable and post this rather than calling performClick directly. //This lets other visual state of the view update before click actions start. if (mPerformClick == null) { mPerformClck = new PeformClick(); } if (!post(mPerformClick)) { performClick(); } } } . . . } break; } . . . return true; }
From the above code, we can see that as long as the CLICKABLE attribute of the View and the LONG_CLICKABLE attribute have a true (the CLICKABLE attribute of the View is related to the specific View, the LONG_CLICKABLE attribute is false by default, and setOnClikListener and setOnLongClickListener will automatically set the above two attributes to true respectively), then this View will consume this touch event, even if the View is in the DISABLED state. If the current event is ACTION_UP, the performClick method will also be called. If the OnClickListener is set, the performClick method will call the onClick method inside it. The code of performClick method is as follows:
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && != null) { playSoundEffect(); (this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; }
The above is a simple summary after I learned to distribute touch events in Android. The narrative in many places is not clear and accurate enough.