Based on Android 28 source code analysis
The event distribution of click events is actually the distribution process of MotionEvent events, that is, when a MotionEvent is generated, the system needs to transmit the event to a specific View, and the delivery process is the distribution process.
Three important methods
First we need to introduce three important methods in the click event distribution process:
dispatchTouchEvent
Used for event distribution. This method must be called if the event can be passed to the current View, and the return result is affected by the current View’s onTouchEvent and the lower View’s dispatchTouchEvent methods, indicating whether the current event is consumed.
onInterceptTouchEvent
Called within dispatchTouchEvent to determine whether to intercept an event. If the current View intercepted an event, this method is not called again within the same sequence of events and returns the result indicating whether to intercept the current event.
onTouchEvent
Called within dispatchTouchEvent to handle click events and returns a result indicating whether the current event is consumed. If not, the current View cannot receive the event again in the same sequence of events.
In fact, their relationship can be expressed as the following pseudocode:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onInterceptTouchEvent(ev)) {
return onTouchEvent(ev);
}
return child.dispatchTouchEvent(ev);
}
Copy the code
For a root ViewGroup, a click event is first passed to its dispatchTouchEvent method. If the onInterceptTouchEvent returns true, it intercepts the current event. The event is then handed to the ViewGroup’s onTouchEvent method. If onInterceptTouchEvent returns false, it does not intercept the current event, which is passed to its children, whose dispatchTouchEvent handles the click event, and so on until the event is finally handled.
Source code analysis of event distribution
When a click event occurs, it is passed in the following order: Activity -> Window -> View. That is, the event is always passed to the Activity, the Activity to the Window, and finally the Window to the top-level View. After receiving the event, the top-level View distributes the event according to the event distribution mechanism.
Activity Distributes click events
// Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Copy the code
Click events are represented by MotionEvent. When a click occurs, the dispatchTouchEvent of the current Activity is used to distribute the event. The specific work is done by the Window inside the Activity. If true is returned, the event loop is complete. Returning false means that the event is not being handled. All View onTouchEvents return false, and the Activity’s onTouchEvent will be called.
Window distribution of click events
Now look at how the Window passes events to the ViewGroup. If you look at the source code, you’ll see that Window is an abstract class, and Window’s superDispatchTouchEvent method is an abstract method, so you have to find the Window implementation class. You can see from the comments that the only implementation of Window is PhoneWindow, so let’s take a look at how PhoneWindow handles click events.
// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
Copy the code
PhoneWindow passes the event directly to the DecorView, GetDecorView ().findViewById(Android.r.i.C.Ontent)).getChildat (0) The mDecor is clearly the View returned by getWindow().getDecorView(), and the View we set with setContentView is a child of it. Since the DecorView inherits child FrameLayout and is the parent View, the final event is passed to the View. From here, the event is passed to the top-level View, that is, the View set by setContentView in the Activity, which is generally a ViewGroup
topView
Distribution of click events
First, the ViewGroup dispatchTouchEvent distribution process is mainly implemented in the ViewGroup dispatchTouchEvent method. This method has a lot of code and is explained in sections.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {...// Check for interception.
final boolean intercepted;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) { // Determine whether to intercept the current event
// Use the FLAG_DISALLOW_INTERCEPT bit to determine whether to intercept
final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
if(! disallowIntercept) { 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
As you can see from the above code, when the event type is ACTION_DOWN or mFirstTouchTarget! = null to determine whether to intercept the current event. The ACTION_DOWN event is easy to understand, so mFirstTouchTarget! What does = null mean? MFristTouchTarget is assigned to the child element when the event is successfully handled by the child element of the ViewGroup. That is, when the event is intercepted by the current ViewGroup and not handled by the child element, MFristTouchTarget == null, then when ACTION_MOVE and ACTION_UP events arrive, Because of the (actionMasked = = MotionEvent. ACTION_DOWN | | mFirstTouchTarget! If this condition is false, the onInterceptTouchEvent of the ViewGroup will not be called again, and all other events in the same sequence will be assigned to the ViewGroup by default.
There is a special case, that is, FLAG_DISALLOW_INTERCEPT tag, the tag bit is set by requestDisallowInterceptTouchEvent method, commonly used in child View. FLAG_DISALLOW_INTERCEPT Once set, ViewGroup cannot block click events other than ACTION_DOWN. Why events other than ACTION_DOWN? This is because the ViewGroup will reset the FLAG_DISALLOW_INTERCEPT bit if it is ACTION_DOWN when distributing the event, invalidating the bit set in the child View.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {...// 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(); // Reset the FLAG_DISALLOW_INTERCEPT flag
}
// Check for interception.
final booleanintercepted; . }Copy the code
In the above code, ViewGroup resets the status of an ACTION_DOWN event, and in resetTouchState resets FLAG_DISALLOW_INTERCEPT, So the child View call requestDisallowInterceptTouchEvent method will not affect ViewGroup ACTION_DOWN event processing.
It can be concluded from the above that when the ViewGroup decides to intercept an event, subsequent click events are assigned to it by default and its onInterceptTouchEvent method is not called. So onIntecepterTouchEvent is not going to be called every time, so if we want to process all the clicks ahead of time, we have to select dispatchTouchEvent, which is the only method that’s guaranteed to be called every time, That is, if the event can be passed to the current ViewGroup.
Now, when a ViewGroup is not intercepting an event, the event is propagated down to its child views for processing
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {...if(! canceled && ! intercepted) {// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null&& childrenCount ! =0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) { // Iterates through all the children of the ViewGroup to determine whether the children can receive the click event
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if(childWithAccessibilityFocus ! =null) {
if(childWithAccessibilityFocus ! = child) {continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child,null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if(newTouchTarget ! =null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // The dispatchTouchEvent method of the child element is actually called
// 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();
// mFirstTouchTarget is assigned and the for loop is broken
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if(preorderedList ! =null) preorderedList.clear();
}
if (newTouchTarget == null&& mFirstTouchTarget ! =null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while(newTouchTarget.next ! =null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; }}}... }Copy the code
The logic of the above code is to first iterate through all the children of the ViewGroup and then determine whether the children can receive the click event. The acceptability of click events is mainly measured by two points:
- Whether the child element is playing an animation
- Click whether the coordinates of the event fall within the region of the child element
If the child element satisfies these two conditions, the event is passed to it for processing. Passed by dispatchTransformedTouchEvent method to complete
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final booleanhandled; .// 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); }...return handled
}
Copy the code
You can see that if the child passes something other than NULL, it calls the child’s dispatchTouchEvent method directly, so that the event is handled by the child, completing a round of event distribution.
If the child’s dispatchTouchEvent returns true, then the mFirstTouchTarget is assigned and the for loop is broken, The actual assignment of mFirstTouchTarget is done by the addTouchTarget function.
// ViewGroup.java
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
Copy the code
As you can see from the code, mFirstTouchTarget is a single linked list data structure. If mFirstTouchTarget is null, then the ViewGroup intercepts all subsequent clicks in the same sequence by default. This point has been analyzed above.
If the event is not handled properly after iterating through all child elements, there are two cases:
ViewGroup
There are no children- The child element handles the click event, but in
dispatchTouchEvent
In the backfalse
That’s usually because the child element is inonTouchEvent
In the backfalse
In both cases, the ViewGroup handles the click itself
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {...// 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
DispatchTransformedTouchEvent in the code in the incoming child is null, the signature analysis can know, it will be called super. DispatchTouchEvent (event), obviously, So we’re going to go to the View’s dispatchTouchEvent method, which is clicking on the event and sending it to the View.
View handles the click event
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {...if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//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; }}...return result;
}
Copy the code
The View’s handling of the click event is simpler because the View (excluding the ViewGroup) is a single element that has no child elements and therefore cannot pass events down, so it has to handle the event itself. If the onTouchListener method returns true, then the onTouchEvent will not be called. As you can see, onTouchListener takes precedence over onTouchEvent, which makes it easier to process the click event from the outside.
// View.java
public boolean onTouchEvent(MotionEvent event) {...final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) { // Unusable views consume click events as well
if(action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) ! =0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
returnclickable; }...// If one of the View's CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE, or TOOLTIP is true, the event will be consumed
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if(! clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress =false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! =0;
if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if(isFocusable() && isFocusableInTouchMode() && ! isFocused()) { focusTaken = requestFocus(); }if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) {// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if(! focusTaken) {// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if(! post(mPerformClick)) { performClickInternal(); }}}if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if(! post(mUnsetPressedState)) {// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break; . }return true;
}
return false;
}
Copy the code
In the above code, the event is consumed whenever one of the View’s CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE, or TOOLTIP is true. The onTouchEvent method returns true regardless of whether it is disabled. Then, when the ACTION_UP event occurs, the performClickInternal method is fired, and finally the performClick method is called.
// View.java
public boolean performClick(a) {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if(li ! =null&& li.mOnClickListener ! =null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
Copy the code
As you can see from the above code, if the View has OnClickListener set, its onClick method will be called inside the performClick method
conclusion
- The same event sequence refers to a series of events generated in the process from the moment the finger touches the screen to the moment the finger leaves the screen
down
The event starts with an indefinite number of intermediatemove
Event, finally toup
End of the event - a
View
Once a decision is made to intercept, the sequence of events can only be handled by it (if the sequence of events can be passed to it), and itsonIntercepetTouchEvent
Will not be called again. This is also easy to understand, that is, when aView
Once you decide to intercept an event, the system hands all other methods in the same sequence of events directly to it, so you don’t have to call it againView
的onIntercepterTouchEvent
Ask it if it’s going to intercept - Normally, a sequence of events can only be one
View
Intercept and consume. The reason for this one can refer to the previous one,Because once an element intercepts this event, all other events in the same sequence of events will be handled by it, so the same sequence of events cannot be handled by twoView
Simultaneous processingBut it can be done through special means, such as oneView
Get things through that you should be handling yourselfonTouchEvent
Force pass to othersView
To deal with. - a
View
Once started processing the event, if it does not consumeACTION_DOWN
Event (onTouchEvent returns false), then no other events in the same sequence of events are assigned to it and the event is reassigned to its parent, that is, the parent elementonTouchEvent
Will be called. It means that once the event is handed to aView
Process, then it must consume, otherwise the rest of the same sequence of events will not be left for it to process. - if
View
Don’t consume exceptACTION_DOWN
Other events, then the click event will disappear, then the parent elementonTouchEvent
Will not be called, and is currentlyView
You can continue to receive subsequent events, and eventually those missing click events will be delivered toActivity
To deal with ViewGroup
Does not intercept any events by default, Android source codeViewGroup
的onInterceptTouchEvent
Method returns by defaultfalse
View
There is noonInterceptTouchEvent
Method, once a click event is passed to it, then itsonTouchEvent
The method will be calledView
的onTouchEvent
By default, both consume events (returntrue
), unless it is unclickable (both clickable and longClickable arefalse
).View
的longClickable
Property defaults tofalse
.clickable
Properties have to be cases, likeButton
的clickable
Property defaults totrue
And theTextView
的clickable
Property defaults tofalse
View
的enable
Attributes do not affectonTouchEvent
The default return value of Even if aView
是disable
State of, as long as it’sclickable
orlongClickable
There is a fortrue
So itsonTouchEvent
It returnstrue
onClick
It’s going to happen if it’s presentView
It’s clickable, and it’s receiveddown
和up
In the event- Event passing is outside-in, meaning that events are always passed to the parent and then to the child
View
Through therequestDisallowInteceptTouchEvent
Method can intervene in the event distribution of a parent element in a child element, butACTION_DOWN
Except for the event - to
View
Set up theOnTouchListener
, its priority ratioonTouchEvent
Be high, ifOnTouchListener
中onTouch
Methodtrue
thenonTouchEvent
Method will not be called. If the returnfalse
, the currentView
的onTouchEvent
The method is called back.
reference
- Android development art exploration