The so-called event distribution of View is actually the distribution process of MotionEvent, that is, when a MotionEvent is generated, the system needs to transmit the event to a specific View, and the transmission process is the distribution process.
The dispatch process is accomplished by three important methods: dispatchTouchEvent, onInterceptTouchEvent, and onTouchEvent. These methods are briefly described below:
1, Public Boolean dispatchTouchEvent(MotionEvent EV) 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.
Public Boolean onInterceptTouchEvent(MotionEvent event) public Boolean onInterceptTouchEvent(MotionEvent Event) This method is not called again and returns a result indicating whether the current event is intercepted.
3, Public Boolean onTouchEvent(MotionEvent event) called in the dispatchTouchEvent method to handle click events, returns a result indicating whether the current event is consumed, if not, in the same sequence of events, The current View cannot receive the event again.
When a click event is generated, the event is always sent to the Activity. The Activity’s dispatchTouchEvent() dispatchTouchEvent dispatchTouchEvent() dispatchTouchEvent is sent to the Activity.
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Copy the code
SuperDispatchTouchEvent (ev) is called to getWindow(). GetWindow () returns a Window object. PhoneWindow is the only implementation of the abstract class Window in Android, so you can directly look at PhoneWindow superDispatchTouchEvent(EV), the source code is as follows: Path com. Android. Internal. Policy. PhoneWindow# superDispatchTouchEvent:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
Copy the code
And call to mDecor superDispatchTouchEvent inside (event), source code is as follows, path com. Android. Internal. The policy. DecorView# superDispatchTouchEvent:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
Copy the code
MDecor is a DecorView object, and DecorView inherits from FrameLayout, which inherits from ViewGroup. FrameLayout itself doesn’t override the dispatchTouchEvent method, So super.dispatchTouchEvent(Event) ends up being called in the ViewGroup, so the core part of event distribution is the ViewGroup, and we’ll leave that behind and focus on it later.
Looking back at the Activity’s dispatchTouchEvent method, the process will be determined based on the return value of the dispatchTouchEvent from the ViewGroup. If getWindow().superDispatchTouchEvent(ev) returns true, the ViewGroup handled the event, or if no one handled the event in the event distribution process. The Activity’s onTouchEvent method is called, which is the overall event distribution process.
ViewGroup dispatchTouchEvent(Event); ViewGroup dispatchTouchEvent The path android.view.ViewGroup#dispatchTouchEvent is a bit longer.
@Override public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; // The return value at the end of the method, which indicates whether the event was handled. If (onFilterTouchEventForSecurity (ev)) {/ / for security reasons, can filter click event, in this method to deal with some of the event flags, returns FALSE discard events, return TRUE to continue processing. final int action = ev.getAction(); // There are a lot of bit-operations in Android source code. The range of bit-operations is limited and then judged. The function of bit-operations here is to reserve the last 8bit of the action and place the rest 0 as actionMasked. final int actionMasked = action & MotionEvent.ACTION_MASK; // Handle an initial down. // Because the event flow starts with the press event, so when the press event arrives, 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); // Clear mFirstTouchTarget resetTouchState(); // Check for interception. Final Boolean intercepted; / / if the sign bit indicates whether or not the intercept events (actionMasked = = MotionEvent. ACTION_DOWN | | mFirstTouchTarget! = null) {// 1 If the event is ACTION_DOWN, Final Boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)! Final Boolean Disallow_Intercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)! = 0; //3 Allow disallowIntercept if (! DisallowIntercept) {// 4 No set disallowIntercept flag intercepted = onInterceptTouchEvent(ev); // It is decided to intercept intercepted to true, followed by ev.setAction(action) at comment 6; // restore action in case it was changed } else { intercepted = false; }} else {// 2 If mFirstTouchTarget has not been processed and is not the original ACTION_DOWN event, it will be intercepted. // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } if (! canceled && ! intercepted) {// 7 no interception, By the child View for processing the if (actionMasked = = MotionEvent. ACTION_DOWN | | (split && actionMasked = = MotionEvent. ACTION_POINTER_DOWN) | | ActionMasked == motionEvent.action_hover_move) {// Notice that ACTION_MOVE and ACTION_UP events are not executed here final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount ! = 0) { final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; I -) {/ / 8 traversal ViewGroup all child View final int childIndex = getAndVerifyPreorderedIndex (childrenCount, I, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); If (dispatchTransformedTouchEvent (ev, false, child, idBitsToAssign)) {/ / 9 calls dispatchTransformedTouchEvent, Internal call child dispatchTouchEvent() newTouchTarget = addTouchTarget(Child, idBitsToAssign); // 10 assign break to mFirstTouchTarget; }}}}} // 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); } else {// there is a child View processing ACTION_DOWN, // Dispatch to touch targets for ACTION_MOVE and ACTION_UP excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target ! = null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {// AlreadyDispatchedToNewTouchTarget to true explains mFirstTouchTarget assigned, so have to sub View, here will return true handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; If (dispatchTransformedTouchEvent (ev, cancelChild, target. The child, target pointerIdBits)) {/ / set the FLAG_DISALLOW_INTERCEPT, The subsequent ACTION_MOVE and ACTION_UP execute the child's dispatchTouchEvent() as handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } } return handled; }Copy the code
First look at mFirstTouchTarget in comment 1, so mFirstTouchTarget! What does = null mean? As you can see from the code logic behind this, when the event is successfully handled by a child of the ViewGroup, mFirstTouchTarget is assigned and points to the child.
That is, mFirstTouchTarget is not null when the ViewGroup does not intercept the event and hands it off to child elements. Otherwise, once the event is intercepted by the current ViewGroup, mFirstTouchTarget is null, so when ACTION_MOVE and ACTION_UP events arrive, mFirstTouchTarget is null, which causes the code in comment 2 to be executed, The onInterceptTouchEvent of the ViewGroup is no longer called, and all other events in the same sequence are handled by default.
Is, of course, there is a special case, the annotation of 3 FLAG_DISALLOW_INTERCEPT tag, the tag is through ViewGroup requestDisallowInterceptTouchEvent methods to set, is generally used in View. FLAG_DISALLOW_INTERCEPT Once set, ViewGroup cannot block click events other than ACTION_DOWN. Why other events besides ACTION_DOWN? This is because the ViewGroup will reset the FLAG_DISALLOW_INTERCEPT bit when it dispatches an event with ACTION_DOWN. This will invalidate the flag bit set in the child View. Therefore, when faced with an ACTION_DOWN event, the ViewGroup will always call its onInterceptTouchEvent method to ask if it wants to intercept the event, which means annotation 4 must be executed. So the child View call requestDisallowInterceptTouchEvent method does not affect the ViewGroup ACTION_DOWN event handling, So FLAG_DISALLOW_INTERCEPT is designed not to intercept subsequent non-ACTION_DOWN events.
If ViewGroup decided to intercept ACTION_DOWN events, will perform 6 dispatchTransformedTouchEvent method, temporarily no matter dispatchTransformedTouchEvent internal first, under the unified analysis, MFirstTouchTarget is null, subsequent non-action_Down events will be handed to it by default and its onInterceptTouchEvent method will not be called
If the ViewGroup does not intercept the event, the event is propagated down to its child views for processing, as specified in comment 7. Then, in comment 8, all child elements of the ViewGroup are iterated to determine whether the child elements can receive the click event. The ability to receive a click event is measured by two factors: whether the child element is playing an animation and whether the coordinates of the click event fall within the child element’s region. If a child element to satisfy the two conditions, the event will be passed to it, and then will call to dispatchTransformedTouchEvent method, temporarily no matter dispatchTransformedTouchEvent internal first, under the unified analysis, If dispatchTransformedTouchEvent returned to the true, will be executed annotation 10, call addTouchTarget () method, internal will complete mFirstTouchTarget assignment, and in the annotations of 11 traversal termination of children elements, AddTouchTarget () : android.view.viewgroup #addTouchTarget () : android.view.viewgroup
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; // mFirstTouchTarget = target; // mFirstTouchTarget is assigned return target; }Copy the code
Whether or not mFirstTouchTarget is assigned will directly affect the ViewGroup’s strategy for intercepting events. If mFirstTouchTarget is null, then the ViewGroup defaults to intercepting all subsequent click events in the same sequence, as previously analyzed.
If the event is not handled properly after iterating through all the child elements, there are two cases: first, the ViewGroup has no child elements; The second is that the child handles the click event, but returns false in dispatchTouchEvent, which is generally because the child returned false in onTouchEvent. In both cases, the ViewGroup will click event, also called to the notes 6 code execution dispatchTransformedTouchEvent method, just as ViewGroup to intercept the events, to deal with.
Mentioned above ViewGroup itself to intercept or child View without treatment, still need to deal with the ViewGroup itself, will be called dispatchTransformedTouchEvent (), child of the incoming parameter is null, if is not intercept, traverse to the child the View, Will call dispatchTransformedTouchEvent (), the incoming parameters that child traverse the View itself, so take a look at below dispatchTransformedTouchEvent (), the source code is as follows, The path to the android. View. ViewGroup# dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Canceling motions is a special case. We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } // Calculate the number of pointers to deliver. final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // If for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. if (newPointerIdBits == 0) { return false; } // If the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. // Otherwise we need to make a copy. final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } // 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); } // Done. transformedEvent.recycle(); return handled; }Copy the code
Extract the core code to see the main execution
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
Copy the code
That is, if the child passed is null, a return super.dispatchTouchEvent() is called, which is dispatchTouchEvent() of the ViewGroup’s parent View, and the click event is sent to the View for processing. If child is not null, then the child view’s dispatchTouchEvent() is called. If the child view is not a ViewGroup, then the view’s dispatchTouchEvent() is called. If the child view is a ViewGroup, The dispatchTouchEvent() method of the ViewGroup is analyzed above
Then the View dispatchTouchEvent(MotionEvent event), directly to the source code is android.view.View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) { // If the event should be handled by accessibility focus first. if (event.isTargetAccessibilityFocus()) { // We don't have focus or no virtual descendant has it, do not handle the event. if (! isAccessibilityFocusedViewOrHost()) { return false; } // We have focus and got the event, then use normal event dispatch. event.setTargetAccessibilityFocus(false); } boolean result = false; if (mInputEventConsistencyVerifier ! = null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } 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)) {// 1 check that mOnTouchListener exists and mOnTouchListener returns true result = true; } if (! Result && onTouchEvent(event)) {//2 If onTouchEvent(event) result = true; } } if (! result && mInputEventConsistencyVerifier ! = null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && ! result)) { stopNestedScroll(); } return result; }Copy the code
As you can see from the code, the View’s handling of the click event is relatively simple, because the View (excluding the ViewGroup here) is a single element, it has no child elements and therefore cannot pass events down, so it has to handle the event itself. If the onTouch method in OnTouchListener 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. If OnTouchListener is not set, the View’s onTouchEvent(MotionEvent Event) method is executed. The code is simplified as follows, and the source path is Android.view. View#onTouchEvent
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; //1 check whether the View is clickable, This includes clicking or holding events if ((viewFlags & ENABLED_MASK) == DISABLED) {//2View is DISABLED 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. return clickable; } if (mTouchDelegate ! = null) {// If the TouchDelegate is set, the TouchDelegate onTouchEvent will be executed, Similar TouchEventListener if (mTouchDelegate onTouchEvent (event)) {return true; } } if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {//3clickable 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; } boolean prepressed = (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(); //4performClick() } } } 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
OnTouchEvent determines whether the View is clickable at comment 1, including click and long press, and then at comment 2, when the View is not available, you can see that the View in the unavailable state consumes click events even though it appears not to be available.
The onTouchEvent method returns true (comment 5). The onTouchEvent method returns true (comment 5). This event is consumed whether the View is disabled or not.
Then when the ACTION_UP event occurs, the performClick method will be fired. This means that if the View has OnClickListener set to execute the code in comment 4, then the performClick method will call its onClick method inside.