“This is my fourth day of the November Gwen Challenge.The final text challenge in 2021”
preface
In Android, the event distribution mechanism is a very important knowledge, master this mechanism can help you in the peacetime development to solve a lot of View event conflict problem, this question is also asked in the interview a lot of questions, this article will summarize the knowledge.
Event Distribution Cause
In Android, the View on the page is displayed in a tree structure, and the View will overlap. When we click on a place where there are multiple views that can respond, who should the click event be given? In order to solve this problem, an event distribution mechanism is needed
Event dispatch object
Touch event, which is passed to the View for each Touch event (MotionEvent), depending on the logic of the receiver
When the user touches the screen, Touch events (encapsulated as MotionEvent objects) are generated, which can be divided into the following categories
- Motionevent. ACTION_DOWN: This event is generated when the finger clicks on the screen. It is the start of all events
- MotionEvent.ACTION_MOVE: This event is generated when a finger is swiped across the screen
- Motionevent. ACTION_CANCLE: Terminates the current event for a non-human cause
- Motionevent. ACTION_UP: This event is generated when a finger leaves the screen
A complete Touch event is the process from the user’s finger touching the screen (accompanied by an ACTIONDOWN event) to the user’s finger leaving the screen (accompanied by an ACTIONUP event)
ACTIONDOWN (once) –> ACTIONMOVE (N times) –> ACTION_UP (once)
Event distribution method
- DispatchTouchEvent (MotionEvent EV) : dispatchTouchEvent(MotionEvent EV) : dispatchTouchEvent(MotionEvent EV) When an event is detected by the underlying driver, it is reported and ultimately handled by the Activity’s method to decide whether to consume it or pass it on
- OnInterceptTouchEvent (MotionEvent EV) : When an event is distributed to a ViewGroup, it can decide whether to intercept the event. Only the ViewGroup has this method
- OnTouchEvent (MotionEvent Event) : This is the last method in the event distribution process, that is, whether or not the event is consumed
Event distribution participant
- Activity: Contains ViewGroup and View
- ViewGroup: contains viewgroups and views
- View: Contains no other views, only itself
The usual event distribution direction is Activity –> ViewGroup –>…… –> View
Note:
- Child View by requestDisallowInterceptTouchEvent methods of intervention in the parent View events distribution (except ACTION_DOWN events), and this is what we deal with conflict of sliding key method in common use
- If the View sets onTouchListener and returns true in the overridden onTouch method, its onTouchEvent method will not be called, Because onTouch takes precedence over onTouchEvent in the dispatchTouchEvent of the View; The onClick method is also not called because onClick is called back in onTouchEvent
Event Distribution Process
- The underlying Input driver reads and writes events from the /dev/inpu/path of the hardware Input device node named event[NUMBER]. (You can use adb shell getevent to view the node under your device.) Android also gets the raw data from these nodes, encapsulates it and provides it to developers; The dispatchTouchEvent method is passed to the DecorView after a series of calls
- In the DecorView, events are passed through the Window’s internal interface, Callback, because the Activity implements that interface and the story is dispatched to the Activity; After the Activity gets the event, it dispatches the event to the window where the Activity is located in the dispatchTouchEvent method. The actual type is PhoneWindow. The window in turn passes the event to its top-level view, the DecorView
- DecorView is a subclass of FrameLayout, a subclass of ViewGroup, that does not process the event itself, but continues to submit the event to the ViewGroup; An event goes from the Activity to the ViewGroup
- The ViewGroup dispatches the event in the dispatchTouchEvent method. If its onInterceptTouchEvent method intercepts the event, it hands it off to its onTouchEvent method. Instead, iterate over its child views and continue to distribute events, stopping as soon as one of the child views consumes the event
- The event is passed to the child View’s dispatchTouchEvent method. If OnTouchListener is registered with the child View and returns true, the event distribution ends there. Otherwise it will continue passing the event to the child View’s onTouchEvent method
- The child View will call back the View’s onClick listener in the ACTION_UP event. If the child View does not consume the event, it will be passed back to the Activity according to the distribution process. If no one else consumes the Activity (including the Activity itself), the event is destroyed
Event distribution source
The following source code is based on API24
Corresponding to the above process, when there is a Touch event, the steps are as follows
DecorView.dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
returncb ! =null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
Copy the code
Cb refers to the Callback interface inside the window, which is implemented by the Activity
Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Copy the code
This method is used by the Activity to handle touch screen events. We can override this method and return true/false so that the event can be intercepted before it is sent to the window. The ViewGroup or View inside the Activity will not receive the event
A touch screen event that starts with ACTION_DOWN must enter the onUserInteraction() method
public void onUserInteraction(a) {}Copy the code
This is an empty method that is called when a button event, touch screen event, or trackball event is dispatched to the Activity; If you want to know how the user interacts with the device while the Activity is running, you can override this method; Note that this method only responds to touch-down gestures, not to subsequent touch-move and touch-up gestures
The counterpart to this method is onUserLeaveHint, which is also an empty method and is called when:
This method is called as part of the Activity lifecycle when the Activity goes into the background while the user is doing something; For example, if the user presses the home button, the current Activity goes into the background and is invoked before onPause. However, this method will not be called if an incoming call causes the Activity to passively enter the background
Next, enter the second if statement
getWindow().superDispatchTouchEvent
GetWindow () gets a Window object, but it is instantiated in the Attach method of the Activity. The actual type is PhoneWindow, and this is where the Callback interface is implemented
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window) {... mWindow =new PhoneWindow(this, window);
mWindow.setCallback(this); . }Copy the code
Here I go to PhoneWindow, as follows
PhoneWindow.superDispatchTouchEvent
// This is the top view of the window
private DecorView mDecor
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
Copy the code
DecorView .superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
Copy the code
So, DecorView is a subclass of FrameLayout, and FrameLayout is a subclass of ViewGroup, so you go to the ViewGroup
ViewGroup.dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// Conformance validator for debugging purposes
if(mInputEventConsistencyVerifier ! =null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
// This variable is used to mark whether the event is consumed
boolean handled = false;
Filter touch events according to the application security policy
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle initialization after initial Down occurs
if (actionMasked == MotionEvent.ACTION_DOWN) {
// The new ACTION_DOWN event is coming, and you need to cancel and clear the previous touch Targets
// Clear the mFirstTouchTarget
cancelAndClearTouchTargets(ev);
// Reset the touch state
resetTouchState();
}
// Flags whether to intercept events
final boolean intercepted;
// When ACTION_DOWN occurs or ACTION_DOWN has already occurred and mFirstTouchTarget is assigned, the ViewGroup is checked to see if it needs to intercept the event.
// Only ACTION_DOWN event occurs, mFirstTouchTarget! = null
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
/ / child View can be set by calling the parent View requestDisallowInterceptTouchEvent method mGroupFlags values
// This tells the parent View whether to intercept the event
final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
// If the child view does not tell the parent not to intercept the event, the parent view determines whether it needs to intercept the event
if(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action);// Resume the action in case it has been changed
} else {
// The child View tells the parent View not to intercept events
intercepted = false; }}else {
// When mFirstTouchTarget=null (no child views are assigned to handle) and it is not an Initial Down event (the event has already been initialized), the ViewGroup continues to intercept touches
// Continue to set to true
intercepted = true;
}
// If the current event is ACTION_CANCEL, or view.mprivateFlags is set to PFLAG_CANCEL_NEXT_UP_EVENT
// Then the current event is cancelled
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
//split indicates whether the current ViewGroup supports splitting motionEvents into different views
final booleansplit = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) ! =0;
/ / new TouchTarget
TouchTarget newTouchTarget = null;
// Whether the event is distributed to the new TouchTarget
boolean alreadyDispatchedToNewTouchTarget = false;
// Enter the zone without canceling the event and without intercepting the event
if(! canceled && ! intercepted) {// Distribute events to all subviews, looking for views that can get focus
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
// If there are three kinds of events, we need to traverse the subview
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;
// Clear earlier touch targets for this PointerId
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
// If the current ViewGroup has child Views and newTouchTarget=null
if (newTouchTarget == null&& childrenCount ! =0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Scan the View from front to back to get the child views that can receive events
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// Go through all the child views and find one to receive the event
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If the current child View does not get focus, skip the child View
if(childWithAccessibilityFocus ! =null) {
if(childWithAccessibilityFocus ! = child) {continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// If the current subview is not visible and does not animate or is not within the range of the touch point, skip this subview
if(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child,null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// if the TouchTarget corresponding to the child View is found in the TouchTarget list, the View is receiving the event, no need to iterate, exit directly
newTouchTarget = getTouchTarget(child);
if(newTouchTarget ! =null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
If the child view returns true, it consumes the event and jumps out of the loop
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Get the TouchDown time
mLastTouchDownTime = ev.getDownTime();
// Get the TouchDown Index
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;
}
// Get the x and y coordinates of TouchDown
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// Add to the list of touch targets while assigning the mFirstTouchTarget value
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) {
// No child View received the event, so assign the last touch target to newTouchTarget
newTouchTarget = mFirstTouchTarget;
while(newTouchTarget.next ! =null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; }}}// mFirstTouchTarget is assigned by the addTouchTarget method;
// The addTouchTarget method is entered only when ACTION_DOWN is handled.
// This is why a View that does not consume ACTION_DOWN events does not receive MOVE,UP, etc
if (mFirstTouchTarget == null) {
// Then the ViewGroup can handle the event itself
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
ACTION_DOWN is received by a sub-view, and subsequent move up events are sent to the touch target
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while(target ! =null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// If view.mPrivateFlags is set to PFLAG_CANCEL_NEXT_UP_EVENT or the event is intercepted by ViewGroup
// The child View needs to cancel the event
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// Continue to distribute events to child views
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue; } } predecessor = target; target = next; }}// Update the touch target list when a lift or cancel event occurs
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
// Remove Pointer from TouchTarget according to idBit if it is a finger lift event under multi-touch
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1<< ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); }}if(! handled && mInputEventConsistencyVerifier ! =null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
Copy the code
This method is a bit verbose and needs to be broken down
Step 1: Event initialization
The first thing that comes in is the ACTION_DOWN event, which requires some initialization:
- The first thing to do is to clear all touchTargets and set mFirstTouchTarget to null; MFirstTouchTarget is also of type TouchTarget, which is an inner class of ViewGroup that describes the view of a touch and the ID of the pointer it captures; MFirstTouchTarget means that if the event is handled by the child View, the mFirstTouchTarget will be assigned and will point to the child View
- The second thing is to reset the status value, via FLAGDISALLOWINTERCEPT resets mGroupFlags value
ViewGroup.cancelAndClearTouchTargets
/**
* 取消和清空所有的 touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if(mFirstTouchTarget ! =null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0 f.0.0 f.0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for(TouchTarget target = mFirstTouchTarget; target ! =null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if(syntheticEvent) { event.recycle(); }}}/** * Clear all the touch targets. */
private void clearTouchTargets(a) {
TouchTarget target = mFirstTouchTarget;
if(target ! =null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while(target ! =null);
mFirstTouchTarget = null; }}/** * resets all touch states in preparation for the new cycle. */
private void resetTouchState(a) {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
Copy the code
Step 2: Intercept judgment
Next, you need to determine whether you need to intercept events:
So let’s first look at the conditions
// Flags whether to intercept events
final boolean intercepted;
// When ACTION_DOWN occurs or ACTION_DOWN has already occurred and mFirstTouchTarget is assigned, the ViewGroup is checked to see if it needs to intercept the event.
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
/ / child View can be set by calling the parent View requestDisallowInterceptTouchEvent method mGroupFlags values
// This tells the parent View whether to intercept the event
final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
// If the child view does not tell the parent not to intercept the event, the parent view determines whether it needs to intercept the event
if(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action);// Resume the action in case it has been changed
} else {
// The child View tells the parent View not to intercept events
intercepted = false; }}else {
// When mFirstTouchTarget=null (no child views are assigned to handle) and it is not an Initial Down event (the event has already been initialized), the ViewGroup continues to intercept touches
// Continue to set to true
intercepted = true;
}
Copy the code
- When the event is an ACTIONDOWN or mFirstTouchTarget! If the event is ACTION = null, the interception will be determined by the first stepOn DOWN, mFirstTouchTarget must be null, so only two cases are entered: ACTIONThe DOWN event comes in and you need to determine the intercept; ACTIONIf a child of the DOWN event receives the event (so that mFirstTouchTarget is assigned), then subsequent events need to determine whether to intercept the event
- The reverse logic of the above condition is that the event is an ACTIONAn event (such as move or Up) after a DOWN event and mFirstTouchTarget is null indicates that the event is in ACTIONThe DOWN event determines that the event needs to be intercepted or that there is no child View to handle the event, so subsequent events need not be distributed and continue to be intercepted
The first if statement contains the interception judgment logic
- First get mGroupFlags value, through calculation and child view can be set by calling the parent view requestDisallowInterceptTouchEvent method mGroupFlags values, tell the parent view don’t intercept events
- If disallowIntercept is true, the child view asks the parent view not to intercept and sets intercepted to false
- If disallowIntercept is false, the child view does not say do not intercept, so call onInterceptTouchEvent to see if you need to intercept the event
ViewGroup.requestDisallowInterceptTouchEvent
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if(disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0)) {
// Return if it has already been set
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Tell the parent view in turn
if(mParent ! =null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); }}Copy the code
ViewGroup.onInterceptTouchEvent
/** * ViewGroup can block all touch events in this method, the default is not to block events, developers can override this method to decide whether to block events * the following four conditions are true, return true, block events * the first: touch events from the mouse pointer device * the second: Is the touch event ACTION_DOWN * Third: checks if the mouse or stylus button (or combination of buttons) is pressed, i.e. the user must actually press * fourth: is the touch point on the scrollbar */
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
Copy the code
Step 3: ACTION_DOWN event distribution
The next step is to iterate through the child View and distribute the ACTION_DOWN event to the child View that can receive the event
- If the current child View does not get focus, the child View is skipped
- If the current child View is not visible and does not animate or is not within the range of the touch point, skip this child View
- If the TouchTarget corresponding to the child View is found in the TouchTarget list, the View is receiving events and does not need to iterate again
- If the child view in the touch position, dispatchTransformedTouchEvent method is invoked by the event distribution to the view, if this method returns true, prove the view consumption this event, then don’t need to look for the child view receive events, jump out of the traverse
ViewGroup.dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// When a cancel operation occurs, no further operations are performed
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;
}
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// For some reason, an inconsistent operation occurs, and the event is discarded
if (newPointerIdBits == 0) {
return false;
}
// The main area of distribution
final MotionEvent transformedEvent;
// Check whether the expected pointer ID is equal to the event pointer ID
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
// When no subview exists, ViewGroup calls view.dispatchTouchEvent to distribute the event and viewGroup.onTouchEvent to handle the event
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
// Distribute touch events to child viewGroups or views;
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY); // Adjust the position of the event
}
return handled;
}
transformedEvent = MotionEvent.obtain(event); // Copy the event to create a new MotionEvent
} else {
// Separate the event to get the MotionEvent containing newPointerIdBits
transformedEvent = event.split(newPointerIdBits);
}
if (child == null) {
// When no subview exists, ViewGroup calls view.dispatchTouchEvent to distribute the event and viewGroup.onTouchEvent to handle the event
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
// Convert the view's matrix
transformedEvent.transform(child.getInverseMatrix());
}
// Distribute touch events to child viewGroups or views;
handled = child.dispatchTouchEvent(transformedEvent);
}
/ / recycling transformedEvent
transformedEvent.recycle();
return handled;
}
Copy the code
This method is where the ViewGroup actually handles events, distributing sub-views to consume events, and filtering out irrelevant pointer ids. When the subview is null, the MotionEvent will be sent to the ViewGroup; Not null, the view.dispatchTouchEvent method is finally called to distribute the event.
This method calls and return to ViewGroup. DispatchTouchEvent will call addTouchTarget method
ViewGroup.addTouchTarget
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
Copy the code
You can see that mFirstTouchTarget is assigned here
When a child control consumes an event, mFirstTouchTarget is not null; MFirstTouchTarget is null when the child control does not consume an event or is intercepted
Step 4: ACTIONMOVE ACTIONUP Event Distribution
After step 3, the ViewGroup might find a child View consumption event
- If the event is intercepted, mFirstTouchTarget== NULL, then the subsequent event eventually calls the view.dispatchTouchEvent method to distribute the event
- If the ViewGroup has no child views, mFirstTouchTarget==null, repeat as above
- If there is a child View, but the child View does not consume the event, mFirstTouchTarget== NULL, then repeat as above
- If there is a child View, and the child View consumption ACTION_DOWN event, but the dispatchTouchEvent returns false (i.e. dispatchTransformedTouchEvent returns false, Then addTouchTarget will not be called), mFirstTouchTarget==null, then do the same
- Now that mFirstTouchTarget is not null, we need to distribute subsequent events to the View consuming ACTION_DOWN events
Through the ViewGroup. DispatchTouchEvent methods of analysis, we know that regardless of the child View consumer events, will enter the final events dispatchTouchEvent method, that we continue to explore
View.dispatchTouchEvent
/** * Passes the touch event down to the target View, or the View is the target View. * *@returnReturn true if the event was consumed, or false */ otherwise
public boolean dispatchTouchEvent(MotionEvent event) {...boolean result = false;
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Before the Down event, it stops if there is a scroll operation. If no, no operation is performed
stopNestedScroll();
}
// Filter touch events to apply security policies
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
// If OnTouchListener is set to the View
// The view is not disabled
// and OnTouchListener. OnTouch returns true
// The View consumes the event and returns true
if(li ! =null&& li.mOnTouchListener ! =null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
Return true if onTouchListener. onTouch has no consumption event and the View's onTouchEvent method returns true
if(! result && onTouchEvent(event)) { result =true; }}// If this is the end of the gesture, clean up after the nested scroll;
// If we try ACTION_DOWN but we don't want the rest of the gesture, cancel it too.
if(actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && ! result)) { stopNestedScroll(); }return result;
}
Copy the code
There are two important points here
- If the developer sets OnTouchListener to listen and returns true in the onTouch method, the view consumes the event
- If no listener is set, the View’s onTouchEvent method is called to handle the event
Ontouchlistener. onTouch takes precedence over onTouchEvent. As long as onTouchListener. onTouch returns true, the onTouchEvent will not be executed
Now let’s look at the logic of onTouchEvent
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();
// If the view is disabled, you can set it to be disabled via setEnabled()
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if(action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) ! =0) {
setPressed(false);
}
// CONTEXT_CLICKABLE (CONTEXT_CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE); // CONTEXT_CLICKABLE (CONTEXT_CLICKABLE, LONG_CLICKABLE)
// The event is still consumed, but there is no response
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
// When the View status is ENABLED
/ / and the view to satisfy CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE one, consumption of this event
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! =0;
if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
// Get focus in touchable 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 the Tap operation, which removes the long-press callback method
removeLongPressCallback();
// If you are in the pressed state, perform the click operation
if(! focusTaken) {// Use Runnable and publish instead of calling performClick directly
// This updates the other visual state of the view before the click begins
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
/ / call the View. An OnClickListener
if(! post(mPerformClick)) { performClick(); }}}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;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Determine if you are in a scrollable view
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
// When in scrollable view, delay TAP_TIMEOUT and feedback the press state to determine whether the user wants to scroll. The default delay is 100ms
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// When no longer scrolling inside the view, feedback the pressing status immediately
setPressed(true, x, y);
According to long / / testing whether, if long press, callback OnLongClickListener. OnLongClick
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if(! pointInView(x, y, mTouchSlop)) {// Outside button
removeTapCallback();
if((mPrivateFlags & PFLAG_PRESSED) ! =0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false); }}break;
}
return true;
}
return false;
}
Copy the code
There are a few things to note here
- As LONG as this view satisfies CLICKABLE, LONGCLICKABLE ,CONTEXTOne type of CLICKABLE, whether disabled or enabled by the setEnabled() setting, returns true as a consumption event
- A View’s longClickable defaults to false. For example, a Button’s clickable defaults to true and a TextView’s clickable defaults to false. But the View’s setOnClickListener will set the View’s clickable to true by default, The View’s setOnLongClickListener also sets the View’s longClickable to true
- In ACTION_DOWN operations, if it is a long press, callback OnLongClickListener. OnLongClick
- In the ACTION_UP action, call onClickListener.onclick
Activity.OnTouchEvent
If there is no View consumption event, it will eventually return to activity.onTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// Loop to determine if there is a ViewGroup or View consumption event, if not, the event returns to the activity
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
Copy the code
Event Distribution Flow Chart
Pay attention to the point
- Touch events by Activity. DispatchTouchEvent processing first; When none of the viewGroups in the middle consumes or intercepts, the View at the bottom level is processed by the OnTouchEvent at the bottom level. If it does not consume, it returns to activity.onTouchEvent
- Only ViewGroup has onInterceptTouchEvent; In the distribution process, any intermediate ViewGroup can intercept directly, so the distribution is not sent down, but is handled by the OnTouchEvent of the ViewGroup that intercepts
- Child View can invoke the parent ViewGroup requestDisallowInterceptTouchEvent method, to set up the disallowIntercept = true, This prevents the parent ViewGroup’s onInterceptTouchEvent from intercepting operations
- When OnTouchEvent bubbles from bottom to top, if OnTouchEvent of any middle layer consumes the event, it is no longer passed up, indicating that the event has been consumed
- If dispatchTouchEvent does not consume an ACTION when the event is dispatchedThe DOWN event, which returns true, is the subsequent ACTIONEvents such as MOVE cannot be received
- CLICKABLE, LONG_CLICKABLE consume events regardless of whether the View is DISABLED or ENABLED
- The View’s setOnClickListener sets the View’s clickable to true by default, The View’s setOnLongClickListener will also set the View’s longClickable to true; SetClickable and setLongClickable for all views are best called after two listener methods
- OnTouch takes precedence over onTouchEvent, onClick and onLongClick are called in onTouchEvent, and onLongClick takes precedence over onClick; If onTouch returns true, onTouchEvent is not executed; OnTouch This method is executed only if the View has OnTouchListener set to enable
This completes the event distribution mechanism and source code analysis.
Three things to watch ❤️
If you find this article helpful, I’d like to invite you to do three small favors for me:
- Like, forward, have your “like and comment”, is the motivation of my creation.
- Follow the public account “Xiaoxinchat Android” and share original knowledge from time to time
- Also look forward to the follow-up article ing🚀