preface
A few days ago, I wrote an article about how a View works, which describes the three main processes of the View. In fact, it is as important as the workflow of the View. There is also a View event distribution mechanism, usually we use setOnClickListener() method to set up a View click listener. Have you ever wondered how the click event gets passed down to this View? When you customize a control, do you return true or false if you want to handle sliding events? And when you encounter sliding nesting situations, how do you resolve sliding nesting conflicts? So, this article takes a look at the source code + flow chart to get an in-depth look at an event distribution mechanism, and once you’ve mastered it, you’ll be much more comfortable with sliding-related problems.
The source of this article is based on Android8.0
Prepare knowledge
1. What are touch events
Touch event is the smallest unit event generated when your hand touches the mobile phone screen. The so-called smallest unit is non-separable. It generally has four types: down, move, up and cancel. Then, the click event, long press event, slide event and so on are composed of several minimum unit events that cannot be separated again.
2. What is MotionEvent
The MotionEvent is Android’s encapsulation of the above touch event information. The event in the event distribution of the View is the MotionEvent. When this MotionEvent is generated, the system will pass this MotionEvent to the View hierarchy. The process of passing a MotionEvent through the View hierarchy is called event distribution. MotionEvent encapsulates both event type and coordinate information.
The event type can be obtained through the motionEvent.getAction() method, which returns a constant corresponding to an event type. There are four main event types:
//MotionEvent.java
public final class MotionEvent extends InputEvent implements Parcelable {
// Press down (down)
public static final int ACTION_DOWN = 0;
// Lift (up)
public static final int ACTION_UP = 1;
// move (move)
public static final int ACTION_MOVE = 2;
/ / cancel (cancel)
public static final int ACTION_CANCEL = 3;
// There are many more
/ /...
}
Copy the code
Coordinate information is also obtained through MotionEvent. Motionevent.getrawx () and motionEvent.getrawy () can obtain coordinate values using the screen as a reference frame. Motionevent.getx () and motionEvent.gety () get coordinate values with the touched View as a reference frame. Refer to the view coordinates below:
Imagine the blue dots are where your fingers touch the screen.
A sequence of events
A series of events generated in the process from finger pressing down to lifting up is an event sequence, which starts with down event, contains an indefinite number of move events in the middle, and finally ends with up event. So there are two possible sequences of events:
- ACTION_DOWN -> ACTION_UP: Finger presses the screen and then lifts it
- ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP: Press your finger on the screen, slide for a while, then lift it up
When analyzing the process of event dispatch, there is the concept of event sequence.
4, the starting point of event distribution, where the event comes from
I think we all know that the View’s dispatchTouchEvent() method starts with the View’s dispatchTouchEvent() method, but if you go back to the View’s dispatchTouchEvent() method, where did the event come from?
There are many kinds of Android input devices, such as screen, keyboard, mouse, trackball, etc. The screen is the device we touch most. When the user touches the screen with his finger, touch events will be generated. The Android input system writes an input device node named event[NUMBER] to the /dev/inpu/path for the touch event, and the EventHub in the input system listens for the input event. The InputReader then reads and processes this raw input event to the InputDispatcher in the input system, The InputDispatcher will find the appropriate WindowHandle(InputWindowHandle type) in the mWindowHandles list (which in IMS represents all Windows), Then the input event is sent through the WindowHandle InputChannel over the Socket to the WindowHandle InputChannel in the Viewrotimp where the application process resides. The ViewRootImp will then distribute the received event to the top-level View held by the ViewRootImp via the internal class InputEventReceiver. The top-level View’s dispatchTouchEvent method will then be called back. Callback(Activity or Dialog implements this interface) and sends the event to the Callback. The Callback’s dispatchTouchEvent method is called back. Different implementation classes have different implementations. In the implementation of an Activity, it hands the event to the PhoneWindow to distribute, which in turn sends the event to the top-level View, which then calls the super.DispatchTouchEvent method, Sending the input event down the View tree until a suitable View is found to handle the event leads to the familiar event distribution mechanism of a View.
The general process for this event transmission is: IMS -> ViewRootImp -> top-level View -> class that implements Window.Callback -> Window -> top-level View. (Check out the original Android touch mechanism for more details.)
EventHub, InputReader, and InputReader are all part of the Android input system. This part is a very complex knowledge, I just want to summarize. So all we need to know is that when the input system listens for the input event, it’s going to give it to the Window, and then the Window is going to give it to the top View, and then the top View is going to distribute it. (See this article on the relationship between Windows, WindowManager, and WindowManagerService for details on the relationship between Windows and Views.)
This top-level View could be a View, it could be a ViewGroup, Depending on whether the View in your addView(View View, viewGroup.layoutParams params) method when you add a Window to WMS is an instance of a View or an instance of a ViewGroup, So the next part of this article will analyze the event distribution of View and ViewGroup respectively.
View event distribution
1, the View: : dispatchTouchEvent ()
The event dispatch of a View is easier than the event dispatch of a ViewGroup, because it’s just a single element, so it only needs to handle its own event. The event dispatch of a View starts with the View’s dispatchTouchEvent() method, so if we look at its dispatchTouchEvent method, As follows:
//View.java
public boolean dispatchTouchEvent(MotionEvent event) {
/ /...
//result defaults to false
boolean result = false;
/ /...
ListenerInfo li = mListenerInfo;
if( li ! =null// If ListenerInfo is not empty&& li.mOnTouchListener ! =null// If the touch event listener is not empty
&& (mViewFlags & ENABLED_MASK) == ENABLED// If the control is ENABLED
&& li.mOnTouchListener.onTouch(this, event)// If onTouch returns true
){
result = true;
}
if (
!result// If none of the above conditions is met, result defaults to false
&& onTouchEvent(event)// If onTouchEvent() returns true
) {
result = true;
}
/ /...
return result;
}
//View.java
static class ListenerInfo {
public OnClickListener mOnClickListener;// Click the event listener
protected OnLongClickListener mOnLongClickListener;// Long press the event to listen
private OnTouchListener mOnTouchListener;// Touch event listener
/ /...
}
Copy the code
As you can see from the pseudo-code of the dispatchTouchEvent() method of the View, the dispatchTouchEvent() method first decides whether to call the View’s onTouchEvent method based on four criteria, as follows:
-
If ListenerInfo is not empty, when is mListenerInfo assigned? When we set a listener to a View, if we set any listener to the View, the mListenerInfo will be initialized first if it hasn’t been initialized yet, for example to set a listener to touch events, let’s look at the setOnTouchListener() method as follows:
//View.java public void setOnTouchListener(OnTouchListener l) { // Initialize mListenerInfo by calling getListenerInfo and assign the touch event listener to mOnTouchListener getListenerInfo().mOnTouchListener = l; } //View.java ListenerInfo getListenerInfo(a) { if(mListenerInfo ! =null) { return mListenerInfo; } mListenerInfo = new ListenerInfo(); return mListenerInfo; } Copy the code
-
If the touch event listener is not empty (i.e. the ListenerInfo mOnTouchListener is not empty), we can see from 1 that when you set the View OnTouchListener, conditions 1 and 2 have been met.
-
3. If the control is ENABLED, that is, the Vew is ENABLED. This condition is true if you have not manually called the View’s setEnable(false) to make the control unavailable.
-
If the onTouch method returns true: this condition is true when you set the OnTouchListener to the View and the onTouch method returns true, indicating that the event was consumed. So at this point, if all four conditions are met, result will equal true, and the View’s onTouchEvent() method will not be called.
But if you don’t set OnTouchListener to your View or if you set OnTouchListener to your View and the onTouch method returns false, as long as one of these two conditions is met, it will keep result as the default value false, Call the View’s onTouchEvent() method if the following conditions are met. Here’s the conclusion: The onTouch method of OnTouchListener takes precedence over the onTouchEvent() method.
Let’s call the View’s onTouchEvent() method, assuming that the above four conditions are not met.
2, the View: : onTouchEvent ()
The onTouchEvent() method handles the View click event and the long press event, calling back the onClick() method and the OnLongClick() method you set for OnClickListener. When you set the OnClickListener or OnLongClickListener callback, you also set your View to clickable state. Some controls are clickable by default, such as Button, Some controls require a click callback or setClickable(true) to click a TextView.
View onTouchEvent();
//View.java
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
CLICKABLE, LONG_CLICKABLE, and CONTEXT_CLICKABLE(callback OnContextClickListener)
// Here we focus on CLICKABLE and LONG_CLICKABLE, i.e. click and long press
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//1. If the View is disabled, it is unavailable
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if(action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) ! =0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// Even if the View is not available, it will consume click events if it can be clicked
return clickable;
}
//2. If the View has a proxy, the TouchDelegate onTouchEvent() method is executed
if(mTouchDelegate ! =null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true; }}//3. Here is how onTouchEvent() handles click events and hold events
// If the control can be clicked
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
/ /...
break;
case MotionEvent.ACTION_DOWN:
/ /...
break;
case MotionEvent.ACTION_CANCEL:
/ /...
break;
case MotionEvent.ACTION_MOVE:
/ /...
break;
}
// If our View is clickable, we must return true, indicating that the event was consumed
return true;
}
//4, finally, although the control is available, but not clickable, return false, do not consume this event
return false;
}
Copy the code
So this onTouchEvent() method is a little bit long, so I’m just capturing the whole frame here, but let’s just make it clear that if onTochEvent() returns true it means that this event is consumed by this View, Returning false means that the View does not consume the event and its parent continues to find the appropriate View to consume. First let’s look at comment 1, which states that even if the View is unavailable, if it can be clicked (clickable = true), it will return true, indicating that the View in the unavailable state will still consume the event, even if the View will not respond, otherwise it will return false; If you want your View to increase its touch range, try using TouchDelegate. If you want your View to increase its touch range, use TouchDelegate. Then comment 3, if the control can be clicked, determine the event type: ACTION_UP, ACTION_DOWN, ACTION_CANCEL, ACTION_MOVE, and then perform different actions according to different event types, and then return true, indicating that the event was consumed. Finally, comment 4 returns false and does not consume the event if the control is not clickable.
Let’s focus on comment 3 to see how onTouchEvent() triggers onClick() and onLingClick() callbacks in ACTION_UP, ACTION_DOWN, ACTION_CANCEL, ACTION_MOVE.
2.1, case ACTION_DOWN:
switch (action) {
case MotionEvent.ACTION_DOWN:
/ /...
// set mHasPerformedLongPress to false
mHasPerformedLongPress = false;
/ /...
// Set mPrivateFlags to PREPRESSED
mPrivateFlags |= PFLAG_PREPRESSED;
//3. Send a task mPendingCheckForTap with postDelayed 100 ms
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
/ /...
break;
}
return true;
Copy the code
Let’s imagine our finger pressing the View goes to the ACTION_DOWN branch. In this branch, in comment 1 it first sets mHasPerformedLongPress to false to indicate that the long press event has not yet been triggered. Then in note 2 to mPrivateFlags set a PREPRESSED identity, said began to check the long press event, then in the annotations 3 through postDelayed send a delayed message, ViewConfiguration. GetTapTimeout () returns 100 milliseconds, MPendingCheckForTap, a CheckForTap type task, is used to detect long press events. Let’s see what this task is, as follows:
//View.java
// Use to detect long press events
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run(a) {
// mPrivateFlags clears PFLAG_PREPRESSED
mPrivateFlags &= ~PFLAG_PREPRESSED;
// See the call chain below, where true is passed to set the PFLAG_PRESSED flag for mPrivateFlags
setPressed(true, x, y);
//3. Call checkForLongClick(), passing in 100 millisecondscheckForLongClick(ViewConfiguration.getTapTimeout(), x, y); }}private void setPressed(boolean pressed, float x, float y) {
/ /...
// Basically calls the setPressed(pressed) method with one argument
setPressed(pressed);
}
// Sets whether the control is in the pressed state
public void setPressed(boolean pressed) {
/ /...
if (pressed) {// If pressed is true
// Set a PFLAG_PRESSED flag to mPrivateFlags
mPrivateFlags |= PFLAG_PRESSED;
} else {// If pressed is false
// Clear the PFLAG_PRESSED flag previously set for mPrivateFlags
mPrivateFlags &= ~PFLAG_PRESSED;
}
/ /...
}
Copy the code
The mPendingCheckForTap run method clears the PFLAG_PREPRESSED flag in mPrivateFlags and sets the PFLAG_PRESSED flag in mPrivateFlags. Most importantly, the mPendingCheckForTap run method in comment 3. CheckForLongClick (); checkForLongClick ();
//View.java
private void checkForLongClick(int delayOffset, float x, float y) {
//1. Check mViewFlags if LONG_CLICKABLE can be pressed
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
// The mHasPerformedLongPress flag bit is false
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
// create a CheckForLongPress task
mPendingCheckForLongPress = new CheckForLongPress();
}
/ /...
/ / 3, ViewConfiguration. GetLongPressTimeout () returns 500 milliseconds, then minus 100 milliseconds to 400 milliseconds
/ / by postDelayed () send delay mPendingCheckForLongPress 400 milliseconds after the execution of taskpostDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); }}Copy the code
This method checks in comment 1 to see if the View can hold a long press event. The View’s LONG_CLICKABLE property is false by default, but setOnLongClickListener () sets it to true. Then in 2 created a CheckForLongPress types of tasks, and then in the annotations 3 through postDelayed () to send a delayed message, which is 400 milliseconds later mPendingCheckForLongPress tasks, it is used to executive according to event, Let’s look at the concrete implementation of this task as follows:
//View.java
// It is used to execute events
private final class CheckForLongPress implements Runnable {
private float mX;
private float mY;
@Override
public void run(a) {
if ((mOriginalPressedState == isPressed())// Check whether the PFLAG_PRESSED flag is cleared in mPrivateFlags. If it is cleared, the long press event is cancelled&& (mParent ! =null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
//2. Call performLongClick()
if (performLongClick(mX, mY)) {
// set mHasPerformedLongPress to true
mHasPerformedLongPress = true; }}/ /...
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true; }}/ /...
}
public boolean isPressed(a) {
// Return true to check that the PFLAG_PRESSED flag is cleared in mPrivateFlags or false to check that the PFLAG_PRESSED flag is cleared
return (mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED;
}
Copy the code
The CheckForLongPress method checks to see if the PFLAG_PRESSED flag is cleared for mPrivateFlags, otherwise the performLongClick() method is called. It will finally call the onLongClick() method callback, if performLongClick() returns true, it will set mHasPerformedLongPress to true, otherwise it will still be false, Whether mHasPerformedLongPress is true depends on whether performLongClick(float x, float y) returns true. As follows:
//View.java
public boolean performLongClick(float x, float y) {
/ /...
final boolean handled = performLongClick();
/ /...
return handled;
}
public boolean performLongClick(a) {
return performLongClickInternal(mLongClickX, mLongClickY);
}
private boolean performLongClickInternal(float x, float y) {
boolean handled = false;
final ListenerInfo li = mListenerInfo;
// If the OnLongClickListener callback is set
if(li ! =null&& li.mOnLongClickListener ! =null) {
// Call the onLongClick method of OnLongClickListener
handled = li.mOnLongClickListener.onLongClick(View.this);
}
/ /...
return handled;
}
Copy the code
I follow through in this method, and I end up in the performLongClickInternal(float x, float y) method, and in the performLongClickInternal() method, If we set the OnLongClickListener callback with setOnLongClickListener(), the familiar onLongClick() method will be called back, Whether performLongClickInternal() returns true depends on whether we return true in the onLongClick() method, Whether performLongClick() returns true depends on whether performLongClickInternal() returns true, and here, combined with the bold text above, comes to a conclusion: If you set onLongClickListener, whether mHasPerformedLongPress is true depends on whether we return true in the onLongClick() method. If not, MHasPerformedLongPress is always false. Whether it is true or not affects the key to whether or not we can call the onClick() method in ACTION_UP.
Now we know from case ACTION_DOWN that if we press the finger and it doesn’t lift within 500 milliseconds, the long-press event will be triggered. Let’s look at ACTION_UP.
And case ACTION_UP 2.2:
switch (action) {
case MotionEvent.ACTION_UP:
/ /...
booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! =0;
// If mPrivateFlags contains PFLAG_PRESSED or PFLAG_PREPRESSED, the if branch will be used
if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
/ /...
if (prepressed) {// If mPrivateFlags contains only the PFLAG_PREPRESSED flag, the user raised his finger within 100 milliseconds and did not execute the CheckForTap task
// Set mPrivateFlags to PFLAG_PRESSED
// Let the user see that the control is still pressed
setPressed(true, x, y);
}
// if mHasPerformedLongPress is false, enter the if branch. MIgnoreNextUpEvent defaults to false
if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) {//4, remove the CheckForLongPress task message, that is, cancel the long press event
removeLongPressCallback();
/ /...
if (mPerformClick == null) {
// If mPerformClick is null, initialize an instance
mPerformClick = new PerformClick();
}
// mPerformClick is added to the message queue by Handler, but the run method in PerformClick still executes the PerformClick () method, so we just look at the PerformClick () method
if(! post(mPerformClick)) {// Execute the PerformClick () method if post a PerformClick failsperformClick(); }}/ /...
Remove CheckForTap task message, that is, cancel CheckForTap
removeTapCallback();
}
break;
}
return true;
//View.java
// The run method in PerformClick still executes the PerformClick () method
private final class PerformClick implements Runnable {
@Override
public void run(a) { performClick(); }}Copy the code
Let’s imagine that we now lift our fingers in three periods of time:
-
If you lift your finger within 100 milliseconds, then mPrivateFlags must only have PFLAG_PREPRESSED and mHasPerformedLongPress is false, according to comments 1 and 3. This will execute the PerformClick() method. Before executing the PerformClick() method, call removeLongPressCallback() in comment 4 to remove the CheckForLongPress task of the long-press event, that is, the onLongClick() callback will not be triggered.
-
2. If you do not lift until 100ms to 500ms, then mPrivateFlags must only be PFLAG_PRESSED and mHasPerformedLongPress is false. The following logic is the same as 1.
-
MPrivateFlags must only be PFLAG_PRESSED if you didn’t lift it after 500 milliseconds. Whether mHasPerformedLongPress is true or not depends on whether we set the onLongClickListener and return true in the onLongClick() method. If you set the onLongClickListener callback and return false in the onLongClick() method or if you don’t set the onLongClickListener callback, you can still go to comment 6 and execute the performClick() method. But if you set the onLongClickListener callback and return true in the onLongClick() method, then you cannot execute the performClick() method.
If you lift your finger within 500 milliseconds, you can only perform a click event, not a press event. If you lift after 500 milliseconds and you set the onLongClickListener and return false in the onLongClick() method or you don’t set the onLongClickListener callback, then you can execute the click event after the long press event, But if you set the onLongClickListener callback and return true in the onLongClick() method, then you cannot execute the click event. PerformClick () is similar to the performLongClick() method in that it finally calls the onClick() method as follows:
//View.java
public boolean performClick(a) {
final boolean result;
final ListenerInfo li = mListenerInfo;
if(li ! =null&& li.mOnClickListener ! =null) {
// Execute onClick() for OnClickListener
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
/ /...
return result;
}
Copy the code
The logic in the performClick() method is that if you set the OnClickListener callback, the onClick() method will be executed, and you’ll notice that performClick() will return either true or false, But this return value has no meaning for the onTouchEvent() method, since the switch block must return true. The onLongClick() method of OnLongClickListener takes precedence over the onClick() method of onClickListener.
Okay, so now that we’ve analyzed the ACTION_DOWN and ACTION_UP branches of onTouchEvent() from press down to lift up, if your finger moves a little bit before lifting, it triggers ACTION_CANCEL or ACTION_MOVE, At this point it will remove the CheckForLongPress or CheckForTap task by calling removeLongPressCallback() or removeTapCallback(), depending on whether the finger has moved out of the View. That is to cancel the long press or click, here is limited to space will not carry out the analysis, you can analyze by yourself.
3, summary
1. The View has no sub-View, so its distribution is relatively simple. The View’s dispatchTouchEvent() method starts from the View’s event distribution process, which is only responsible for event distribution and does not handle actual events. The onTouch() method of the external setting onTouchListener. The onTouchEvent() method of the View.
If onTouchListener is set, the onTouch method will be called back, depending on the return value of the onTouch() method. If true, the onTouchEvent() method will not be called. The dispatchTouchEvent() method returns true; If false is returned, the onTouchEvent() method is called, and how the event is handled depends on the value returned by onTouchEvent(). In onTouchEvent(), whether the control is available or not depends on whether the control is clickable, If the control is clickable (Clickabale or longClickabale, as long as either is true), onTouchEvent() returns true, if the control is not clickable (both Clickabale and longClickabale are false), OnTouchEvent () returns false.
3. If we set OnTouchListener, OnLongClickListener, and OnClickListener callbacks at the same time, the events will be delivered in the order of priority: OnClick () -> onLongClick() -> onClick() -> onClick() -> onClick() -> onClick();
4. For the ViewGroup (the parent of the current View), it only knows the child View’s dispatchTouchEvent() method, not the other two methods that handle the event. The onTouch() and onTouchEvent() subviews are called within their own dispatchTouchEvent(), and they affect the return value of dispatchTouchEvent(). But for the parent ViewGroup, it only knows the return value of dispatchTouchEvent().
Flow chart:
ViewGroup event distribution
1, ViewGroup: : dispatchTouchEvent ()
ViewGroup is a subclass of View, which is a collection of views. It contains many sub-views and sub-viewgroups, so the event distribution of ViewGroup is more complex than that of View, but the event distribution of ViewGroup is the essence of the whole event distribution mechanism. Like a View, a ViewGroup’s event distribution starts with dispatchTouchEvent(). Although this method is in the View, the ViewGroup overrides it because their distribution logic is different. So let’s look at the ViewGroup dispatchTouchEvent() method as follows:
//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
/ /...
// Result of this event
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//1. If the event is ACTION_DOWN
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Empty mFirstTouchTarget, which is of type TouchTarget and is a single linked list structure
cancelAndClearTouchTargets(ev);
// Clear the FLAG_DISALLOW_INTERCEPT flag in mGroupFlags, which is the same flag as disallowIntercept below
resetTouchState();
}
// Whether the ViewGroup intercepts the event flag
final boolean intercepted;
// if the event is ACTION_DOWN or mFirstTouchTarget is not null
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
// Whether the child View disallows the ViewGroup from intercepting event flags
final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
if(! disallowIntercept) {// If the subview allows the ViewGroup to intercept events
// The onInterceptTouchEvent() method is called to ask the ViewGroup whether to intercept events. The value of intercepted is determined by onInterceptTouchEvent(ev)
intercepted = onInterceptTouchEvent(ev);
/ /...
} else {// If the subview disallows the ViewGroup to intercept events
intercepted = false;/ / intercepted a false value}}else {// If the event is not ACTION_DOWN and there is no target
// the intercepted value is true, after which all sequences of events in the current sequence of events are processed by the ViewGroup and are not passed to the child View
intercepted = true;
}
/ /...
// Check whether this event is ACTION_CANCEL
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
//split defaults to true
final booleansplit = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) ! =0;
//newTouchTarget Is used to record the target of the event
TouchTarget newTouchTarget = null;
// Indicates whether the event has been dispatched to the child View corresponding to target. Default is false
boolean alreadyDispatchedToNewTouchTarget = false;
//3. If the event is not cancelled and not intercepted, find the appropriate sub-view to handle
if(! canceled && ! intercepted) {// Retrieve index by pressing the finger
final int actionIndex = ev.getActionIndex(); // always 0 for down
//getPointerId indicates the id of the pressed finger according to index. The first finger is 0, the second finger is 1, and so on
//idBitsToAssign is related to multi-touch and is not discussed in this article
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
/ /...
// If the event is ACTION_DOWN or ACTION_POINTER_DOWN or ACTION_HOVER_MOVE
// This article focuses on ACTION_DOWN events. ACTION_POINTER_DOWN relates to multi-touch
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE
) {
final int childrenCount = mChildrenCount;
// If target is null and the ViewGroup has child views, look for a child View as target
if (newTouchTarget == null&& childrenCount ! =0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final View[] children = mChildren;
// Fetch the child views one by one from back to front
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// Check whether the child View can accept the click event: the child View is visible or playing an animation, and the touch point is in the child View range
if(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child,null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// This means that the subview meets the conditions for handling events
/ /...
/ / dispatchTransformedTouchEvent () will be called the son of View dispatchTouchEvent () method, in this way to hand out events to View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
/ /...
/ / if dispatchTransformedTouchEvent () returns true, said find a child View consumption, the incident will go here, so the child View will be regarded as the target, The addTouchTarget() method is called to create a TouchTarget for the child View, insert the target into the mFirstTouchTarget header, and assign the header return to newTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
/ / alreadyDispatchedToNewTouchTarget assignment to true, said the event has been distributed to target the corresponding View
alreadyDispatchedToNewTouchTarget = true;
break;
}
/ /...
}//end... For ()
/ /...
}//end... if(newTouchTarget == null && childrenCount ! = 0)
}//end... if(actionMasked == MotionEvent.ACTION_DOWN...)
}///end... if(! canceled && ! intercepted)
// If mFirstTouchTarget is null, perform different actions
if (mFirstTouchTarget == null) {// mFirstTouchTarget is null in three cases:
//1, ViewGroup has no child View;
//2. The child View handles the ACTION_DOWN event but returns false at dispatchTouchEvent();
// The ViewGroup onInterceptTouchEvent(ev) returns true
// In all three cases the ViewGroup handles the event itself
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else { // There are two cases where mFirstTouchTarget is not null, indicating that the appropriate child View is found as target:
ACTION_DOWN = ACTION_DOWN = ACTION_DOWN = ACTION_DOWN;
MFirstTouchTarget = mFirstTouchTarget; mFirstTouchTarget = mFirstTouchTarget; mFirstTouchTarget = mFirstTouchTarget
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//mFirstTouchTarget is a single list structure
while(target ! =null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {// The processing of case 1
/ / because when find target has been invoked dispatchTransformedTouchEvent (), said the target View has consumed the event, handle directly to true
handled = true;
} else {// The processing of case 2
// Note intercepted, if true, cancelChild will be true, causing the child View to receive an ACTION_CANCEL indicating that the child View's event is cancelled
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
/ / call dispatchTransformedTouchEvent event () method have distributed to target the corresponding View
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
// Whether handle is true depends on the child View's dispatchTouchEvent() return value
handled = true;
}
// If you need to cancel the event, clear the target corresponding to the child View and remove the tareget from the linked list, so that the child View can not receive the subsequent events of the event sequence
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
// Continue distribution to the next target
target = next;
}//end... while (target ! = null)
}//end... if (mFirstTouchTarget == null)
/ /...
}//end... if (onFilterTouchEventForSecurity(ev))
/ /...
return handled;
}
Copy the code
This method is very long, which is the entire ViewGroup event distribution logic, I know you do not want to see the desire, this method corresponding to the flow chart is as follows:
As you can see, within the same event sequence (from down to up), the ViewGroup dispatchTouchEvent() method can be divided into two processes: 1. 2. Event processing flow except ACTION_DOWN. Now follow these two processes respectively.
ViewGroup processes ACTION_DOWN events
ACTION_DOWN event processing flow can be divided into two processes: ViewGroup interception event (intercepted = true) and non-interception event (intercepted = false).
Looking at the flowchart, the if statement in the dispatchTouchEvent() method comment 2 determines the value of intercepted as follows:
// Whether the ViewGroup intercepts the event flag
final boolean intercepted;
// if the event is ACTION_DOWN or mFirstTouchTarget is null
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
// whether the child View forbids the ViewGroup to intercept event flags
final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
if(! disallowIntercept) {// If the subview allows the ViewGroup to intercept events
// The onInterceptTouchEvent() method is called to ask the ViewGroup whether to intercept events. The value of intercepted is determined by onInterceptTouchEvent(ev)
intercepted = onInterceptTouchEvent(ev);
/ /...
} else {// If the subview disallows the ViewGroup to intercept events
intercepted = false;/ / intercepted a false value}}else {
/ /...
}
Copy the code
If the event is ACTION_DOWN, the if branch is also entered. Check mGroupFlags for FLAG_DISALLOW_INTERCEPT. By default, disallow_intercept is false. So the onInterceptTouchEvent() method is called to ask the ViewGroup whether to intercept events. The value of intercepted is determined by onInterceptTouchEvent(). OnInterceptTouchEvent () returns false by default, so intercepted = false.
2.1、intercepted = false
When the DOWN event is not intercepted by the ViewGroup, intercepted = false, it enters the if statement of the dispatchTouchEvent() method comment 3, as follows:
// Check whether this event is ACTION_CANCEL
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
/ /...
//newTouchTarget Is used to record the target of the event
TouchTarget newTouchTarget = null;
// Indicates whether the event has been dispatched to the child View corresponding to target. Default is false
boolean alreadyDispatchedToNewTouchTarget = false;
//3. If the event is not cancelled and not intercepted, find the appropriate sub-view to handle
if(! canceled && ! intercepted) {/ /...
// If the event is ACTION_DOWN or ACTION_POINTER_DOWN or ACTION_HOVER_MOVE
// This article focuses on ACTION_DOWN events. ACTION_POINTER_DOWN relates to multi-touch
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
// If target is null and the ViewGroup has child views, look for a child View as target
if (newTouchTarget == null&& childrenCount ! =0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final View[] children = mChildren;
// Fetch the child views one by one from back to front
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//3.1. Check whether the child View can accept the click event: the child View is visible or playing animation, and the touch point is in the child View range
if(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child,null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// This means that the subview meets the conditions for handling events
/ /...
/ / 3.2, dispatchTransformedTouchEvent () will be called the son of View dispatchTouchEvent () method, in this way to hand out events to View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
/ /...
/ / 3.3, if dispatchTransformedTouchEvent () returns true, said find a child View consumption, the incident will go here, so the child View will be regarded as the target, The addTouchTarget() method is called to create a TouchTarget for the child View, insert the target into the mFirstTouchTarget header, and assign the header return to newTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
/ / alreadyDispatchedToNewTouchTarget assignment to true, said the event has been distributed to target the corresponding View
alreadyDispatchedToNewTouchTarget = true;
break;
}
/ /...
}//end... For ()
/ /...
}//end... if(newTouchTarget == null && childrenCount ! = 0)
}//end... if(actionMasked == MotionEvent.ACTION_DOWN...)
}///end... if(! canceled && ! intercepted)
Copy the code
If it is a DOWN event, the ViewGroup will enter a for loop, and the ViewGroup will iterate through all the child views. In comment 3.1, the ViewGroup will first determine whether the child View meets the criteria for receiving events. If not, the ViewGroup will look for the next child View. Comments came to 3.2, and then call dispatchTransformedTouchEvent () method to see if the child View consumption DOWN event.
DispatchTransformedTouchEvent (ev, false, child, idBitsToAssign) method is as follows:
//ViewGroup.java
/ / dispatchTransformedTouchEvent () only need to focus on two parameters:
//@params Cancel Whether to cancel the event
//@params Child is ready to receive the child View of the distribution event
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
//1. Enter the if branch if cancel is true
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// Sets the ACTION_CANCEL event
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
/ /... What's missing is the multi-touch processing
//2. Enter the if branch if cancel is false
if (child == null) {// If child is empty
// Call super.dispatchTouchEvent(event), which means the ViewGroup decides whether to process the event
handled = super.dispatchTouchEvent(event);
} else {// If child is not empty
/ /...
// Call child.dispatchTouchEvent(event) to let the child View decide whether to handle the event
handled = child.dispatchTouchEvent(event);
}
return handled;
}
Copy the code
Call child.dispatchTouchEvent(Event) to let the child View decide whether to process the event. At this point, the DOWN event is passed to the child View. If a subview is a View, then its process is the same as the event distribution of the View described earlier. If a subview is a ViewGroup, then its process is the event distribution of the ViewGroup.
Well, suppose son consumption this event, the View returns true, then dispatchTransformedTouchEvent () returns true, ViewGrou found to consume the DOWN child View of events, The addTouchTarget(Child, idBitsToAssign) method is called as follows:
//ViewGroup.java
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
// First get a target association for the View passed in
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
// Then insert the target into the head of the mFirstTouchTarget list
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;//mFirstTouchTarget moves back to the head of the list
// Return the target of the list header
return target;
}
//ViewGroup::TouchTarget
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
/ /...
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
Copy the code
If a child View is found to consume the DOWN event, a Target association is created for that child View, and the child View is assigned to target’s child field, and the target is inserted into the list header and returns, The addTouchTarget method returns and assigns a value to the newTouchTarget field. If the mFirstTouchTarget is not null when the next event comes, it will iterate through the list to find the corresponding target, and distribute the event directly to the View recorded in the target, no longer having to iterate through the ViewGroup subview.
So what are target, mFirstTouchTarget, newTouchTarget we talked about above? They are both of type TouchTarget, as follows:
///ViewGroup::TouchTarget
private static final class TouchTarget {
// View of the current consumption event
public View child;
// Its next node
public TouchTarget next;
// The number of digits pressed on the child is recorded by the number of digits
// For example, pointerIdBits = 001 indicates one finger, pointerIdBits = 011 indicates two fingers,
public int pointerIdBits;
/ /...
}
Copy the code
It is a linked list structure, where Child represents the View that needs to consume the event, next represents the next node, pointerIdBits represents the number of fingers pressed on the child, mFirstTouchTarget is the head of the list, and the list is connected by a number of targets. NewTouchTarget is the newly inserted target. Why is mFirstTouchTarget a linked list? For example, I can touch 5 sub-views in the list with 5 fingers at the same time. If all the 5 sub-views want to consume the DOWN event, they will be recorded in a linked list. When the next event comes, all 5 sub-views can be distributed to the event. So when you see the word target, always remember that it is a linked list.
Multi-touch is another point of knowledge in event distribution, in multi-touch:
1. If multiple fingers are pressed in the same View, the View will receive ACTION_DOWN events from the first finger and then ACTION_POINTER_DOWN events from other fingers.
2. If multiple fingers are pressed in different views, each View will receive an ACTION_DOWN event for the corresponding finger;
In Android, multi-touch event distribution is realized through idBitsToAssign, mFirstTouchTarget and pointerIdBits in mFirstTouchTarget. If you are interested, you can know by yourself: “Event distribution is only ACTION_DOWN once, ACTION_UP once”. .
Ok, so now that we’ve found the child View that can consume the event, and we’ve associated a target with that child View with the addTouchTarget method, we’ve inserted the mFirstTouchTarget, and the mFirstTouchTarget has been moved to the head of the list when the list is inserted, Not null, then a break breaks out of the for loop and goes directly to comment 4 of the dispatchTouchEvent() method as follows:
// If mFirstTouchTarget is null, perform different actions
if (mFirstTouchTarget == null) {
/ /...
} else {// There are two cases where mFirstTouchTarget is not null, indicating that the appropriate child View is found as target:
ACTION_DOWN = ACTION_DOWN = ACTION_DOWN = ACTION_DOWN;
ACTION_DOWN (); // The event is not an ACTION_DOWN event, but the child View of the ACTION_DOWN event is already identified as target, so the event is directly distributed to target's child
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//mFirstTouchTarget is a single list structure
while(target ! =null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {// The processing of case 1
/ / because when find target has been invoked dispatchTransformedTouchEvent (), said the target View has consumed the event, handle directly to true
handled = true;
} else {// The processing of case 2
/ /...
}
predecessor = target;
// Continue distribution to the next target
target = next;
}//end... while (target ! = null)
}//end... if (mFirstTouchTarget == null)
Copy the code
MFirstTouchTarget is not empty, so we go to the else branch, and the else branch is a list of targets, going through all the targets, finding the target of the child View that consumed the DOWN event in the for loop above, the if branch of case 1, Found in the for loop above child View, this View has been spending DOWN event, alreadyDispatchedToNewTouchTarget has been assigned to true, so the handle directly to true.
At this point, the analysis is done without the ViewGroup intercepting the DOWN event. So let’s say we found the child View and the child View consumes the event, so that when the next event comes, mFirstTouchTarget is not empty, we just give that event to the child View; But if it is to find the child View and the View not consumption this DOWN event, namely child View dispatchTouchEvent () method returns false, so dispatchTransformedTouchEvent () returns false, MFirstTouchTarget cannot be assigned, mFirstTouchTarget is null, and when the next sequence of events comes, the ViewGroup will process it directly instead of forwarding it to the child View. The conclusion here is that if a child View does not consume ACTION_DOWN events, then all other events in the same sequence of events are no longer assigned to it, but to its parent ViewGroup. Once the child View consumes the ACTION_DOWN event, all other events in the same sequence are handed over to it.
So if the child View doesn’t consume the ACTION_DOWN event, or IF I override the onInterceptTouchEvent() of the ViewGroup and return true, then the ViewGroup will start intercepting the event, Next look at the ViewGroup interception under the DOWN event, intercepted = true.
2.2, intercepted = true
If the ViewGroup intercepts a DOWN event, intercepted = true does not enter the if statement in comment 3 of the dispatchTouchEvent() method, so that the ViewGroup does not traverse its child views under a DOWN event, Also cannot call dispatchTransformedTouchEvent () found to consume the event View, similarly cannot call addTouchTarget () method for mFirstTouchTarget assignment, This causes mFirstTouchTarget to be null under the DOWN event, which leads directly to the if statement in comment 4 of the dispatchTouchEvent() method, as follows:
// Check whether this event is ACTION_CANCEL
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
/ /...
// If mFirstTouchTarget is null, perform different actions
if (mFirstTouchTarget == null) {// mFirstTouchTarget is null in three cases:
//1, ViewGroup has no child View;
//2. The child View handles the ACTION_DOWN event but returns false at dispatchTouchEvent();
// The ViewGroup onInterceptTouchEvent(ev) returns true
// In all three cases the ViewGroup handles the event itself
// Notice that the third argument is passed null, indicating that the ViewGroup handles the event itself
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
/ /...
}//end... if (mFirstTouchTarget == null)
Copy the code
Obviously here is the situation 3, because there is not find a View, dispatchTransformedTouchEvent () method of the third parameter is empty, and the second parameter is false, because not ACTION_CANCEL event, We refer to the above dispatchTransformedTouchEvent () method, as follows:
//ViewGroup.java
/ / dispatchTransformedTouchEvent () only need to focus on two parameters:
//@params Cancel Whether to cancel the event
//@params Child is ready to receive the child View of the distribution event
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
/ /...
//2. Enter the if branch if cancel is false
if (child == null) {// If child is empty
// Call super.dispatchTouchEvent(event), which means the ViewGroup decides whether to process the event
handled = super.dispatchTouchEvent(event);
} else {// If child is not empty
/ /...
// Call child.dispatchTouchEvent(event) to let the child View decide whether to handle the event
handled = child.dispatchTouchEvent(event);
}
return handled;
}
Copy the code
It calls super.dispatchTouchEvent(Event), which means that the ViewGroup decides whether to process the event, and the parent class of the ViewGroup is View, So the processing logic of super.dispatchTouchEvent(event) is the processing logic of the event distribution of the View, see the previous analysis of the event distribution of the View.
At this point, the analysis of ViewGroup interception under the DOWN event is complete. If the ViewGroup returns true in the ACTION_DOWN event of the onInterceptTouchEvent() method, the entire sequence of events will be handled by the ViewGroup, not the child View.
Going back to the dispatchTouchEvent() method, one other thing to note is that under ACTION_DOWN both intercepting and not intercepting will enter the if statement at comment 1 in the dispatchTouchEvent() method as follows:
//1. If the event is ACTION_DOWN
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Empty mFirstTouchTarget, which is of type TouchTarget and is a single linked list structure
cancelAndClearTouchTargets(ev);
// Clear the FLAG_DISALLOW_INTERCEPT flag in mGroupFlags, which is the same flag as disallowIntercept below
resetTouchState();
}
Copy the code
Before this if statement’s role is to prevent a sequence of events affect the sequence of events, so it will be to call first cancelAndClearTouchTargets empty mFirstTouchTarget (ev), Then call resetTouchState() to clear the FLAG_DISALLOW_INTERCEPT bit, since the ACTION_DOWN event is the start of a new sequence of events, So the first thing that the dispatchTouchEvent() method does is to determine if a new sequence of events is coming in, so determine if it’s an ACTION_DOWN event, and if it’s an ACTION_DOWN event, as the beginning of a sequence of events, The possible legacy of the previous sequence of events should be eliminated. More on the FLAG_DISALLOW_INTERCEPT flag later.
Now that the ViewGroup has analyzed the process of processing ACTION_DOWN events, let’s look at the process of processing events other than ACTION_DOWN events.
3. ViewGroup processes events other than ACTION_DOWN
ACTION_DOWN event processing flow can be divided into two flows: mFirstTouchTarget! = NULL and mFirstTouchTarget == null If it’s true, it doesn’t go into comment 3 of the dispatchTouchEvent() method at all, even if false goes into comment 3 of dispatchTouchEvent(). It also does not satisfy the conditions of annotation 3.1. So let’s go straight to note 4.
3.1, mFirstTouchTarget == null
// Check whether this event is ACTION_CANCEL
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
/ /...
// If mFirstTouchTarget is null, perform different actions
if (mFirstTouchTarget == null) {// mFirstTouchTarget is null in three cases:
//1, ViewGroup has no child View;
//2. The child View handles the ACTION_DOWN event but returns false at dispatchTouchEvent();
// The ViewGroup onInterceptTouchEvent(ev) returns true
// In all three cases the ViewGroup handles the event itself
// Notice that the third argument is passed null, indicating that the ViewGroup handles the event itself
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
/ /...
}//end... if (mFirstTouchTarget == null)
Copy the code
It’s possible to do 1, 2, and 3, and we know from ACTION_DOWN that the assignment to mFirstTouchTarget only happens when you’re dealing with ACTION_DOWN, So if the ViewGroup has no child views while processing ACTION_DOWN events, it will not enter the for loop, causing mFirstTouchTarget to be null; If the ViewGroup has a child View and enters the for loop, but the View does not consume the DOWN event, then false is returned at dispatchTouchEvent() and the addTouchTarget() method cannot be called to assign mFirstTouchTarget. Causes mFirstTouchTarget to be null; The ViewGroup onInterceptTouchEvent(ev) in the DOWN event returns true, does not enter the if statement in comment 3, resulting in null mFirstTouchTarget; So if the mFirstTouchTarget is not found in the ACTION_DOWN event, then mFirstTouchTarget == null in the event other than ACTION_DOWN, then the ViewGroup is left to handle the event itself.
3.2, mFirstTouchTarget! = null
// If mFirstTouchTarget is null, perform different actions
if (mFirstTouchTarget == null) {
/ /...
} else {// There are two cases where mFirstTouchTarget is not null, indicating that the appropriate child View is found as target:
ACTION_DOWN = ACTION_DOWN = ACTION_DOWN = ACTION_DOWN;
ACTION_DOWN (); // The event is not an ACTION_DOWN event, but the child View of the ACTION_DOWN event is already identified as target, so the event is directly distributed to target's child
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//mFirstTouchTarget is a single list structure
while(target ! =null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {// The processing of case 1
/ /...
} else {// The processing of case 2
If intercepted is true, cancelChild will be true, causing the child View to receive an ACTION_CANCEL indicating that the event is cancelled
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
/ / 4.2, call dispatchTransformedTouchEvent distributed to target the event () method
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
// Whether handle is true depends on the child View's dispatchTouchEvent() return value
handled = true;
}
If you need to cancel the event, clear the target corresponding to the sub-view and cancel the tareget from the linked list, so that the sub-view can no longer receive the subsequent events of the event sequence
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
// Continue distribution to the next target
target = next;
}
predecessor = target;
target = next;
}//end... while (target ! = null)
}//end... if (mFirstTouchTarget == null)
Copy the code
mFirstTouchTarget ! = null, which means that mFirstTouchTarget has been found when ACTION_DOWN is handled, and it goes to the else branch of comment 4, which is case 2, which goes to the else branch of case 2, The value of cancelChild in note 4.1 determines whether the child View receives an ACTION_CANCEL event or another event. The value of cancelChild depends on the value of intercepted. So if the onInterceptTouchEvent(ev) method of the ViewGroup returns true in an event other than ACTION_DOWN, cancelChild = true, cancelChild = true, If the ViewGroup defaults to intercepted = false, cancelChild = false, Then in note 4.2 the cancelChild and target. The child pass into the dispatchTransformedTouchEvent () method.
I’ll post the dispatchTransformedTouchEvent () method code, as follows:
//ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
//1. Enter the if branch if cancel is true
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// Sets the ACTION_CANCEL event
event.setAction(MotionEvent.ACTION_CANCEL);
// Distribute the ACTION_CANCEL event
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
/ /... Omit multi-touch processing
//2. Enter the if branch if cancel is false
if (child == null) {
// Call super.dispatchTouchEvent(event), which means the ViewGroup decides whether to process the event
handled = super.dispatchTouchEvent(event);
} else {
/ /...
// Call child.dispatchTouchEvent(event) to let the child View decide whether to handle the event
handled = child.dispatchTouchEvent(event);
}
return handled;
}
Copy the code
You can see that if cancel is true, the if branch in comment 1 will set an ACTION_CANCEL event and pass it to the child View of the target record; If cancel is false, go to the else branch of comment 2 and call child.dispatchTouchEvent(Event) to let the child View of the target record decide whether or not to process the event, as described earlier.
Ok, now we get out of the dispatchTransformedTouchEvent () method, to note 4.3, if cancelChild is true, is called TouchTarget recycler of () method to the target, and what is the consequences of this? In this way, the target corresponding to the child View is emptied and the tareget is cancelled from the linked list, resulting in the subsequent events of the event sequence that the child View can no longer receive.
At this point the ViewGroup is done analyzing the flow of events other than ACTION_DOWN.
4, how does a child View prevent ViewGroup from intercepting events
The onInterceptTouchEvent() function returns true from the onInterceptTouchEvent() function, but the child View still wants to respond to the event. Android provides us a method: requestDisallowInterceptTouchEvent (Boolean) is used to set whether to allow interception, as follows:
//ViewGroup.java
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
/ /...
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
When the View call getParent. RequestDisallowInterceptTouchEvent (true), mGroupFlags will have FLAG_DISALLOW_INTERCEPT logo, When the View call getParent. RequestDisallowInterceptTouchEvent (false), mGroupFlags will eliminate FLAG_DISALLOW_INTERCEPT logo, How does FLAG_DISALLOW_INTERCEPT control ViewGroup interception? As follows:
final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
if(! disallowIntercept) {// If the subview allows the ViewGroup to intercept events
// The onInterceptTouchEvent() method is called to ask the ViewGroup whether to intercept events. The value of intercepted is determined by onInterceptTouchEvent(ev)
intercepted = onInterceptTouchEvent(ev);
/ /...
} else {// If the subview disallows the ViewGroup to intercept events
intercepted = false;/ / intercepted a false value
}
Copy the code
Child View by calling getParent. RequestDisallowInterceptTouchEvent (true), and to prohibit ViewGroup intercept besides ACTION_DOWN of other events, so that the arrival will be handed over to an event this View,
Why events other than ACTION_DOWN? Since the ACTION_DOWN event is the start of a sequence of events, the ACTION_DOWN event will first pass through the onInterceptTouchEvent() method of the ViewGroup, If the ViewGroup returns true from ACTION_DOWN on onInterceptTouchEvent(), It does not enter the if statement in comment 3 of the dispatchTouchEvent() method, so that mFirstTouchTarget cannot be found under the DOWN event, so that when other events in the same sequence of events arrive, mFirstTouchTarget == null, So the ViewGroup can only handle the event itself, it can’t pass it to the child View, so it can’t call the child View’s dispatchTouchEvent() method, This way the View in the dispatchTouchEvent () method call getParent. RequestDisallowInterceptTouchEvent (true) it’s no meaning.
5, summary
Several conclusions can be drawn from the ViewGroup event distribution:
If the onInterceptTouchEvent() method returns true from the ACTION_DOWN event, the whole sequence of events will be handled by the ViewGroup, not the child View. Leading to unable to invoke the child View dispatchTouchEvent () method, which lead to the child View call getParent. RequestDisallowInterceptTouchEvent (true).
If onInterceptTouchEvent() intercepts an event other than ACTION_DOWN, the child View will receive an ACTION_CANCEL event, and all subsequent events will be handled by the ViewGroup.
3, 1, and 2 all mean that the ViewGroup decides to intercept the event, so once the ViewGroup decides to intercept the event, the subsequent events are handed over to the ViewGroup. And the ViewGroup onInterceptTouchEvent() method is no longer called in this event sequence, The onInterceptTouchEvent() method of the ViewGroup is not called every time. Only the dispatchTouchEvent() method of the ViewGroup is guaranteed to be called every time.
3. The ACTION_DOWN event is responsible for looking for the target in the ViewGroup, that is, looking for the sub-view that can consume ACTION_DOWN events. If the sub-view is found, all subsequent events in the same event sequence will be handed over to the sub-view instead of the ViewGroup. If not, there are two scenarios: The ViewGroup has no child View. The child View handles ACTION_DOWN events, but returns false at dispatchTouchEvent(), so all subsequent events in the same sequence are handled by the ViewGroup itself.
4. If a child View does not consume ACTION_DOWN events, then other events in the same sequence of events are not assigned to it, but to its parent ViewGroup. Once the child View consumes an ACTION_DOWN event, if the ViewGroup does not intercept it, all other events in the same sequence of events are handed over to the child View.
5. When super.dispatchTouchEvent(event) is called, the ViewGroup begins to process the event itself, which will execute the ViewGroup onTouchEvent() logic as the View event distribution.
conclusion
When the click event arrives at the ViewGroup, its dispatchTouchEvent() method is called. If the onInterceptTouchEvent() method returns true, it intercepts the current event, It then handles all events within the sequence of events, with the super-.dispatchTouchEvent () method called; If the onInterceptTouchEvent() method of the ViewGroup returns false, it does not intercept the current event. The current event is passed to its child View, and its dispatchTouchEvent() method is called. If the subview is a View, then it’s going to do the same thing as the event distribution of the View, if the subview is a ViewGroup, then it’s going to do the event distribution of the ViewGroup, recursively, from top to bottom, until the whole View tree receives the event, Then recursively, from bottom to top, each layer returns a value that determines whether to consume the event. If it consumes, it returns true, and its upper layer cannot process the event. If it does not consume, it returns false, and its upper layer passes on to the next layer until it reaches the root view.
View event distribution summary and ViewGroup event distribution summary can be found in the source code to prove, you can verify, this article through the source code + flow chart illustrates the whole View event distribution system, in the process of looking at the best to combine the context, always remember that this is in the same event sequence, Follow each branch of the flowchart through the source code so you have a deeper understanding.
References:
Android event distribution fully parses where events come from
Analyze Android event distribution through flow charts
Android Touch Events InputManagerService