Sequence of event distribution
Activity->Window->DecorView->ViewGroup->View
Type of event
ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL
Usually an event sequence is such an action_ The down event is the starting point of an event, followed by multiple actions_ Down event, followed by ACTION_DOWN, an action may be received in the middle_ Down event
Event distribution for Activity
// dispatchTouchEvent method of Activity public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
It can be seen that the Activity actually calls the superDispatchTouchEvent method of Window, and the implementation class of Window is PhoneWindow. Therefore, we can directly view the superDispatchTouchEvent method of PhoneWindow
Event distribution for Window
// superDispatchTouchEvent method of PhoneWindow @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
It is found that the superDispatchTouchEvent method of DecorView is called directly, and then check it further
//superDispatchTouchEvent method of DecorView public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
Originally, the dispatchTouchEvent method of ViewGroup is called here, that is, the events on the interface are directly passed to the dispatchTouchEvent method of the root layout
Analyze how the ViewGroup distributes events. Take a look at the dispatchTouchEvent method of the ViewGroup
Before analyzing the dispatchTouchEvent method, take a look at the dispatchtransformatedtouchevent method. The dispatchtransformatedtouchevent method will be called many times inside the dispatchTouchEvent method
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // If the operation is cancelled, the cancellation event will be distributed directly final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); // If the passed in child is not empty, call the dispatchTouchEvent method of the child; otherwise, call its own dispatchTouchEvent method if (child == null) { //It is equivalent to calling the dispatchTouchEvent method of the View class handled = super.dispatchTouchEvent(event); } else { //Continue distributing events to child views handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } ...... // If the passed in child is not empty, call the dispatchTouchEvent method of the child; otherwise, call its own dispatchTouchEvent method if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { ...... handled = child.dispatchTouchEvent(transformedEvent); } ...... return handled; }
As you can see, the dispatchTransformedTouchEvent method mainly does two things
- If the incoming event is action_ If cancel or cancel parameter is true, action will be distributed directly_ Cancel event
- During the distribution process, if the child is empty, the super.dispatchTouchEvent method of the current View will be called because the dispatchTouchEvent method of ViewGroup will be overridden. At this time, calling the super method is to call the dispatchTouchEvent method of View; If child is not empty, the dispatchTouchEvent method of this child View will be called.
Let's analyze the core code of dispatchTouchEvent
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ...... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 1. Initialize the down event, clear TouchTargets and touchstate, mfirsttouchtarget = null, disallowIntercept=false if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } // 2. Check whether it is intercepted final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // Whether to force not to allow interception. The child View can set the parent to force not to allow interception. The default is false final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { //Ask yourself whether to intercept the event intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { intercepted = true; } ...... // 3. If it is not intercepted, handle the DOWN event first, mainly assigning TouchTarget TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { ...... if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { ...... final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { ...... for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); ...... // Find the child View that is Visible and in the click range if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } ...... // This is equivalent to calling the dispatchTouchEvent method of the child View if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ...... // Assign TouchTarget and refresh the flag bit mFirstTouchTarget newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } ...... } ...... } ...... } } // 4. Do you want to handle the event yourself or let the child View handle the event if (mFirstTouchTarget == null) { // If there is no child View consumption event, it will be consumed by itself, which is equivalent to calling the super.dispatchTouchEvent method handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; // If it is a DOWN event, the dispatchTouchEvent method of the child View has been called above, and nothing needs to be done if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { // Decide whether to force the event to CANCEL event according to intercepted final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // This is equivalent to calling the dispatchTouchEvent method of the child View. If intercepted=true, the action will be forcibly changed to CANCEL; If intercepted=false, then if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } // If intercepted=true, set mFirstTouchTarget to null if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } ...... } ...... return handled; }
The dispatchTouchEvent method is mainly composed of four modules
- Initialize the DOWN event, clear TouchTargets and touchstate, mfirsttouchtarget = null, disallowIntercept=false
- Check for interception
- If it is not intercepted, handle the DOWN event first, mainly assigning TouchTarget
- Do you want to handle the events yourself or let the child View handle the events
At this point, the dispatchTouchEvent of ViewGroup is finished. Let's summarize
The down event ViewGroup will clear mFirstTouchTarget and reset disallowuncept flag to false, so every time the down event comes, the parent container will call onInterceptTouchEvent to ask whether it is intercepted. If the down event is intercepted, the down event will be handed over to ViewGroup for processing. At this time, mFirstTouchTarget=null, so when the next move/up event comes, The parent container directly intercepted=true, so once the ViewGroup intercepts the down event, the subsequent events will be handled by itself, and the child View will not receive any events
When the down event ViewGroup is not intercepted, the ViewGroup will traverse the child views to find the View that can handle the event. If it is not found, the same event will still be handled by ViewGroup, and subsequent events will be handled by him. When the ViewGroup finds the child View that handles the down event, it will assign mFirstTouchTarget, that is, mFirstTouchTarget when the next move event comes= Null
When the move event comes, the ViewGroup still calls back to use onInterceptTouchEvent to ask whether it is intercepted. If it is intercepted, mFirstTouchTarget=null. According to the above analysis, this time the child View will receive a cancel event, and the ViewGroup will clear mFirstTouchTarget=null. Since mFirstTouchTarget=null, all subsequent events will be handled by the ViewGroup itself.
Analyze how the View distributes events. dispatchTouchEvent(MotionEvent ev)
public boolean dispatchTouchEvent(MotionEvent event) { boolean result = false; //Here, you can judge whether the mtouchlistener is set, whether it is ENABLED, and the return value of onTouch //So the priority of ontouch > ontouchevent if (onFilterTouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } //onTouchEvent will be called here if (!result && onTouchEvent(event)) { result = true; } } return result; }
To sum up, first of all, if the onotouchlistener is set, its priority is greater than the onTouchEvent. onTouchEvent will not be called when its onTouch method returns true
Analyze how View consumes onTouchEvent(MotionEvent ev) of events
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); //Here you can judge whether the view is clickable final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; //If the view status is disabled, events can be consumed as long as it is clickable, but no response is made if ((viewFlags & ENABLED_MASK) == 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; } //How to set mTouchDelegate here? mTouchDelegate.onTouchEvent(event) will be called first if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } //As long as it can be clicked, it will come in, and eventually return true consumption event if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ... //Click event onClick is handled here performClickInternal(); break; case MotionEvent.ACTION_DOWN: .... if (!clickable) { checkForLongClick(0, x, y); break; } .... if (isInScrollingContainer) { .... postDelayed(mPendingCheckForTap, } else { .... checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: .... case MotionEvent.ACTION_MOVE: .... break; } return true; } return false; }
To sum up, events can be consumed as long as the View can be clicked. If mTouchDelegate is set, the onTouchEvent method of mTouchDelegate will be executed first, and onLongClick will be executed in action_down, finally to
In the performLongClick method, see the following
public boolean performLongClick() { return performLongClickInternal(mLongClickX, mLongClickY); } private boolean performLongClickInternal(float x, float y) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnLongClickListener != null) { handled = li.mOnLongClickListener.onLongClick(View.this); } if (!handled) { final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y); handled = isAnchored ? showContextMenu(x, y) : showContextMenu(); } if ((mViewFlags & TOOLTIP) == TOOLTIP) { if (!handled) { handled = showLongClickTooltip((int) x, (int) y); } } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
Get called. onClick is executed in action_up, finally to
In the performClick method, see the following
public boolean performClick() { .... 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; } return result; }
, priority onlongclick > onclick
Solutions to incident conflicts
When an event comes, more than one View can handle the event. At this time, the parent container does not know who will distribute the event for processing. At this time, an event conflict occurs. Through the above analysis, we can get two ideas. The first external interception method is to change the return value of the onIntercept method of ViewGroup. The second one interferes with the distribution process of ViewGroup by changing the disallowIntercept flag
Idea 1: external interception method template code
public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if (Event required by parent container) { intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } return intercepted; }
Idea 2: internal interception method template code
public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if (Event required by parent container) { getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } return super.dispatchTouchEvent(ev); }