Make writing a habit together! This is the sixth day of my participation in the “Gold Digging Day New Plan · April More text Challenge”. Click here for more details.
Recently, I need to do some complicated custom views, among which the processing of event distribution is inevitable. Combined with the large amount of information I have read before, the work is completed, but the processing of event distribution always feels very unclear, and the feeling of knowing what is going on makes people very uncomfortable. If you don’t know how event distribution works, it can be difficult to solve complex situations. Before also read Ren Yugang “Android development art Exploration” for the event distribution source analysis, but can only say that the general understanding of the event distribution process, but I do not know the truth.
Simply follow the words of a certain master
“Read the fucking source”
Source code reading is always painful and happy, one is because the source code is very long logic is very complex, two is because the source code to consider too many things, so the interference is too much, often follow up to get lost.
Since this article is a source level analysis, if you have not yet understood the basic process of event distribution, it is best to take a look at this information.
First of all, about the event distribution, I believe that everyone who has contacted with it has seen a U-shaped picture like this:
We also know the functions of dispatchTouchEvent, InterceptTouchEvent and onTouchEvent to distribute events, intercept events and consume events respectively.
I’ve seen similar pseudo-code:
public boolean dispatchTouchEvent(MotionEvent ev) { boolean result = false; // Default is no consumption if (! OnInterceptTouchEvent (ev)) {result = child.dispatchTouchEvent(ev); } if (! Result) {// If the event is not consumed, ask itself onTouchEvent result = onTouchEvent(ev); } return result; }Copy the code
In fact, if you are familiar with these, simple event distribution can be handled, but these are simply memorizing the flow, do not know the specific events in the source code.
In general, viewgroups and views are tree structures, and event distribution is a process of traversing from parent node to child node until it finds a View that can consume events. In this process, it is a process of recursively calling the above pseudocode. The child View always returns the value to the parent View after the execution of dispatchTouchEvent. The parent View determines whether it needs to consume the event according to whether the child View consumes the event, and then returns a Boolean value to its parent View indicating whether it or its child View consumes the event. If the current View (ViewGroup) does not consume itself or its child views, it will pass the event to the parent View’s onTouchEvent method, thus rendering the u-shaped image above. After analyzing the source code, we can understand the subtlety of the design
With regard to the event distribution source, I think it’s best to simulate an event flow, follow the code, try to avoid the interference of various other codes (such as security checks), and start with the simplest events, i.e. touch mode with one finger, the control does not slide, and the ACTION_CANCEL event is not considered.
This article is based on Android23 source code analysis. Please refer to the View and ViewGroup source code for this article
The first step is to make it clear that a sequence of events refers to multiple events in the process of pressing, sliding, and lifting a finger. These are DOWN, MOVE, and UP events.
Here, imagine A scenario with an Activity that has A layout of the outermost ViewGroup A (fills the screen), ViewGroup B (fills the screen), and A Button C (in the middle of the screen).
Case 1: The InterceptTouchEvent (InterceptTouchEvent) intercepts the event completely (InterceptTouchEvent returns true), now clicks on any point in range A, slides and lifts. (Activity, PhoneWindow, and DecorView distributions are not analyzed)
Now the event is ACTION_DOWN. First, the DecorView calls the dispatchTouchEvent method of ViewGroup A (of course, only the key code is selected here) and you see line 2103 of ViewGroup:
// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! Final Boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)! = 0; if (! intercepted = onInterceptTouchEvent(ev); ev.setAction(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. intercepted = true; }Copy the code
This paragraph is simple, but important. The key is intercepted = onInterceptTouchEvent(ev); A intercepts the event, so intercepted is true.
So, line 2134:
if (! canceled && ! intercepted)Copy the code
The statement inside will not be executed. (The statement inside is basically going through the child View to find the child View that consumes the sequence of events.)
Then come to line 2239:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
Copy the code
MFirstTouchTarget = null mFirstTouchTarget = null mFirstTouchTarget = null
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
Copy the code
DispatchTransformedTouchEvent here is very important, but also pay attention to the third parameter to null.
Enter dispatchTransformedTouchEvent method, first look at the comments:
Transforms a motion event into the coordinate space of a particular child view,filters out irrelevant pointer ids, and overrides its action if necessary. If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
If child (the third argument) is null, the event is passed to the current ViewGroup, A.
The corresponding code in dispatchTransformedTouchEvent approach, at a ViewGroup line 2567:
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
Copy the code
Yes, it’s called now
handled = super.dispatchTouchEvent(transformedEvent);
Copy the code
It’s just the View’s dispatchTouchEvent method.
In the dispatchTouchEvent View, look at line 9285 of the View:
if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li ! = null && li.mOnTouchListener ! = null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (! result && onTouchEvent(event)) { result = true; }}Copy the code
You can see that if the View has onTouchListener set and returns true, the onTouchEvent will not be executed and the whole dispatchTouchEvent method will return this result.
View defaults onTouchListener to null, so onTouchEvent will be executed.
The onTouchEvent defaults to events such as OnClickListener and OnLongClickListener. View 10288 line:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
Copy the code
If the condition is not true, that is, if the View is not Clickable (the default state), onTouchEvent returns false and does not execute click events.
If A is the default, then A’s dispatchTouchEvent returns false to the DecorView, meaning that neither A itself nor its child views consume the event. Since the DecorView has only one child, View A, if A does not consume events, subsequent events, including moves and UP, will not be distributed to A.
If A is Clickable (for example, OnClickListenrer is set), onTouchEvent returns true, which is returned to the DecorView, which passes subsequent events to it for processing. When the ACTION_UP event is passed, onTouchEvent will trigger the click events of OnClickListenrer and so on.
As for why, listen to the following breakdown. Here, you can know, in the case of ViewGroup intercept events, will by dispatchTransformedTouchEvent to call their own. Super dispatchTouchEvent method call onTouchEvent method finally, That is, treat yourself as a View to handle events.
DispatchTransformedTouchEvent is critical, the specific instructions below ~ ~
Scenario 2: A does not intercept the event, B intercepts the event, taps any point on the screen with one finger, then slides and lifts.
Again, first DecorView calls the dispatchTouchEvent method of ViewGroup A, at which point intercepted is already false, so it goes to line 2134 of ViewGroup
if (! canceled && ! intercepted)Copy the code
You need to enter (cancel is false in this case, otherwise false).
Now we go to line 2144 of the ViewGroup:
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
Copy the code
At present, we only look at the first judgment actionMasked == MotionEvent.ACTION_DOWN. Actually, this judgment statement is used to find sub-views that can consume Down events.
Start with line 2155 of ViewGroup:
final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount ! = 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Scan children from front to back. // Final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus ! = null) { if (childWithAccessibilityFocus ! = child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); // If the View does not contain touch points continue; }Copy the code
So obviously, the ViewGroup is traversing the child views, so the buildOrderedChildList method is creating a collection of child views to traverse from the top to the bottom, so the View at the top has the highest priority to consume events. The last mainly USES isTransformedTouchPointInView method to determine whether a touch point on the child View.
B is A child of A, and the touch point is on B, so continue is not executed, that is, the code that follows. Go to line 2199 of ViewGroup:
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList ! = null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; }Copy the code
See isTransformedTouchPointInView method, again into the method, skip about ACTION_CANCEL and refers to the code of a touch more, mainly is the code:
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
Copy the code
There are two cases of whether the child is null or not. The case of null is not discussed here, where the child is the View that traverses and determines that the touch point is in it.
When child is not null, some sliding offsets are done and the child’s dispatchTouchEvent method is called.
Yes, this is where event distribution is really implemented, starting with passing events to the dispatchTouchEvent method of the child View. In have sure touch point on the View of the situation, the role of call dispatchTransformedTouchEvent method is through the View of dispatchTouchEvent return values to judge whether the View to consume this event, true for consumption, False does not consume.
Now the child View is ViewGroup B, which intercepts events, so interceptTouchEvent returns true. All B’s will execute the View’s dispatchTouchEvent method as A did in case 1, and then call onTouchEvent. By default, onTouchEvent returns false, so it will not go to the previous A
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
Copy the code
ViewGroup 2239 line instead:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
Copy the code
Yeah, mFirstTouchTarget here is in
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
Copy the code
The judgment statement is assigned a value so that it holds the View that consumed the event. If B’s onTouchEvent returns true, it will only be assigned if B consumes the event. It is a linked list of the TouchTarget class inside the ViewGroup. The main reason it is a linked list is to refer to touch cases, but we will assume that it represents the View that consumes the event.
MFirstTouchTarget == null; mFirstTouchTarget == null;
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
Copy the code
Noticed at this time the child pass null parameters, looked back at the code of dispatchTransformedTouchEvent knew that will be called super. DispatchTouchEvent, source of annotation about this:
As mentioned earlier, if there is no child View consumption, the current ViewGroup itself calls dispatchTouchEvent to try to consume it.
What if B can consume events (B’s onTouchEvent returns true)? So when traversing A to B, A statement of the dispatchTransformedTouchEvent will return true (when this time B has performed onTouchEvent method), it can perform listed earlier, ViewGroup the start of the 2201 lines of code.
Focus on line 2215 of ViewGroup:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
Copy the code
Look at the addTouchTarget method:
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
Copy the code
Insert a new head node in the list that starts with mFirstTouchTarget. Regardless of the multi-touch case, this is analogous to assigning a Child to mFirstTouchTarget.
The key here is that mFirstTouchTarget is assigned, it holds the child View of A that needs to consume the event, and then in the rest of the dispatchTouchEvent code, it returns true if mFirstTouchTarget is not null, Report to DecorView that A’s child View or has its own View consumption event.
So what does mFirstTouchTarget mean? Remember that there is a rule in event distribution: once a View consumes a DOWN event, subsequent events in the series are handled by that View.
Now that the DOWN event is over, here comes the MOVE event. The same dispatchTouchEvent method from A goes to ViewGroup 2144:
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE)
Copy the code
This is an ACTION_MOVE event, but of course it can’t go inside the statement. So A doesn’t execute the View that can consume the event, and jumps directly to the 2240 line of ViewGroup code mentioned earlier. Then enter line 2244 else statement, where the key is line 2251:
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
Copy the code
AlreadyDispatchedToNewTouchTarget said whether the newly added TouchTarget, this in an event on the DOWN time is set to true, but in this case because there is no add new TouchTarget, so false.
So it goes to line 2256:
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
Copy the code
In this case, target.child is the View held in mFirstTouchTarget, in this case ViewGroup B. So by dispatchTransformedTouchEvent we know that there will be the current event ACTION_MOVE passed dispatchTouchEvent method B.
In general, the consumption View B of the DOWN event is saved via mFirstTouchTarget, and subsequent events are passed directly to B’s dispatchTouchEvent.
If B wants to consume the MOVE event, handle is assigned true, and A’s dispatchTouchEvent returns Handle true. If B does not consume the MOVE event, then A’s dispatchTouchEvent returns false to the DecorView.
Scenario 3: A does not block the event, and B does not block the event, and clicks the Button with one finger, then slides and lifts.
It’s actually very similar to scenario 2.
DecorView calls A’s dispatchTouchEvent method. A, because onInterceptTouchEvent returns false, walks through the child View to B and calls B’s dispatchTouchEvent method. B) The onInterceptTouchEvent returns false. The dispatchTouchEvent method returns true for Button C, which is Clickable. So B logs C in B’s mFirstTouchTarget, and then B’s dispatchTouchEvent returns true to A, telling it “I can handle this event on my end”, and THEN A logs B in A’s mFirstTouchTarget, A’s dispatchTouchEvent method returns true to the DecorView.
So when the MOVE event comes down, A looks directly for the View held by A’s mFirstTouchTarget, which is B, and B looks directly for the View held by B’s mFirstTouchTarget, which is C, and if C wants to consume the event, it returns true all the way up.
What if C doesn’t consume events at this point? C’s dispatchTouchEvent returns false, B’s dispatchTouchEvent returns false, and B’s dispatchTouchEvent cannot call its own super.dispatchTouchEvent yet, so false is returned to A, A is the same as B, So this event ends up being handed over to the Activity because of the recursion.
This is the event distribution rule: “If the View does not consume other events other than DOWN, the parent View will not call onTouchEvent to process this event, and the View can still receive subsequent events, which are not handled by the View are handed to Activtiy for processing”.
What if the View doesn’t consume DOWN events? In fact, the previous case 1 has been briefly described, but it is more intuitive to combine A,B, and C with the latter two cases. If C does not accept DOWN events, B’s onTouchEvent method will process the event (regardless of the onTouchListener case). If B cannot consume DOWN events, A’s onTouchEvent method will be called to process the event. So you have this u-shaped situation where you’re throwing things up. If B can consume the DOWN event, then C’s mFirstTouchTarget is recorded as B, and THEN B’s mFirstTouchTarget is null, so when the subsequent event comes to C, C is directly handed over to B, and B doesn’t iterate through the sub-view because the event is no longer ACTION_DOWN. Check whether mFirstTouchTarget is null. MFirstTouchTarget is still null because there is no traversal of the sub-View, so B will call its own super-.dispatchTouchEvent, and by the same reasoning as the previous analysis, Subsequent events will not be received by C (look again at line 2144 of ViewGroup).
This corresponds to another rule of event distribution: once a View does not consume events in onTouchEvent, subsequent events are not handed over to it. (If the DOWN touch point is in multiple views, should we say that none of these views consume the event?)
Finally, after a series of events has ended, a new series of events will arrive, clearing the last saved state (e.g. MFirstTouchTarget), look at the ViewGroup dispatchTouchEvent in line 2094:
// Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); }Copy the code
So much for the event distribution mechanism, there are a lot of things left unmentioned, such as multi-touch, CANCEL events, etc. Because of the recursion involved in event distribution, it is sometimes easy to get lost as one layer comes in and another comes out. I am also on the source code research is not in-depth, there are omissions or mistakes, I hope you correct ~~
Original is not easy, if you feel that this article is helpful to yourself, don’t forget to click on the likes and attention, but also to the author’s affirmation ~