Event Passing#
Events are passed from Activity to View layer by layer.
Concept of Event Dispatching#
It is mainly accomplished through 3 methods.
Key Dispatch Methods | Function | Return Significance |
---|---|---|
Dispatch Touch Event: Boolean | Triggered by the upper layer View, passed to the target View | The return result is influenced by the parent and child Views (true: handled) onTouchEvent dispatchTouchEvent |
onInterceptTouchEvent: Boolean | Used in the current View to determine whether to intercept an event | The return result indicates whether the event is intercepted (true: intercepted) |
Touch Event: Boolean | The current View has intercepted and started processing | The return result indicates whether the event is consumed (true: handled) |
public boolean dispatchTouchEvent(MotionEvent e) {
bool isEventConsume = false;
if(onInterceptTouchEvent(e)) {
isEventConsume = onTouchEvent(e);
} else {
isEventConsume = child.dispatchTouchEvent(e);
}
return isEventConsume;
}
It can be seen that the dispatch order is dispatchTouchEvent -> onInterceptTouchEvent
, deciding the subsequent path based on whether the event is consumed. The recursive call of dispatchTouchEvent
continues until it finds the View that consumes the event. The View is a tree structure; if the View has intercepted the click event, it will trigger onTouch
. If onTouch
does not consume the event, it will be passed to the onTouchEvent
method, where the onClick
event will be present.
Activity Receives Events#
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// Here, getWindow refers to the PhoneWindow class
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//PhoneWindow.java
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView.java
//DecorView inherits from FrameLayout, and FrameLayout does not override
//dispatchTouchEvent method, so it must look for its parent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
// ViewGroup.java
// ViewGroup inherits from View
// ViewGroup overrides dispatchTouchEvent, so no need to continue to View
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
}
Event Handling#
Through the above analysis, we know how the click event is passed to DecorView#ViewGroup
. Here we will further analyze the ViewGroup click event. First, let's understand a few MotionEvents:
MotionEvent Event | Action | Others |
---|---|---|
ACTION_DOWN | Finger down | Can be intercepted or not |
ACTION_UP | Finger up | Event ends |
ACTION_MOVE | Sliding on screen | Will be triggered multiple times |
ACTION_CANCEL | Event canceled | Triggered when the event is intercepted by the upper layer |
ViewGroup Handling - ChildView Intercepts ACTION_DOWN
#
The Down event will only be triggered once (single touch, multiple touches will not be just once). Interception means that the ViewGroup handles the event itself and will not dispatch it to ChildView. Each click event starts with Action Down; please pay attention to the following annotations, and it mainly does the following (here we will first list the main handling items):
Clear previous events: Note resetTouchState method, which will be executed when the ViewGroup#dispatchTouchEvent event is ACTION_DOWN.
Note
Clear previous events: Note resetTouchState method, which will be executed when the ViewGroup#dispatchTouchEvent event is ACTION_DOWN.
// ViewGroup.java
// Start from this ViewGroup, passing down to the target of the click
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// Get the current action
final int action = ev.getAction();
// Perform an AND operation with the Mask to get the real Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Execute reset only during Action Down
// @ Track cancelAndClearTouchTargets method
cancelAndClearTouchTargets(ev);
// @ Track resetTouchState method
resetTouchState();
}
}
...
return handled;
}
private void resetTouchState() {
// @ See clearTouchTargets method
clearTouchTargets();
resetCancelNextUpFlag(this);
// Clear FLAG_DISALLOW_INTERCEPT, allowing ViewGroup to intercept
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
// Clear all events of the first clicked View chain
private void clearTouchTargets() {
// TouchTarget is a singly linked list
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
// Recycle TouchTarget for reuse
target.recycle();
target = next;
} while (target != null);
// Set member mFirstTouchTarget to null
mFirstTouchTarget = null;
}
}
Whether to call ViewGroup#onInterceptTouchEvent method: There are two conditions, plus a FLAG check:
- Currently is ACTION_DOWN event
- A ChildView has already handled this click event (if a child View handles the event, it will assign a value to mFirstTouchTarget)
- Whether the current ViewGroup is prohibited from intercepting (generally when ViewGroup receives an event, if it is not prohibited from intercepting, it will execute the onInterceptTouchEvent method)
ACTION_DOWN
// ViewGroup.java
// Start from this ViewGroup, passing down to the target of the click
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// Get the current action
final int action = ev.getAction();
// Perform an AND operation with the Mask to get the real Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Initialize FLAG_DISALLOW_INTERCEPT during Action down
final boolean intercepted;
// There are two checks here to determine whether to call ViewGroup's own onInterceptTouchEvent method
// 1. Currently is ACTION_DOWN
// 2. A child View has already handled this click event
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// Check whether interception is prohibited
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// Call its own onInterceptTouchEvent method
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
...
return handled;
}
If the ViewGroup itself does not intercept, it will recursively call ChildView: executing ViewGroup#dispatchTransformedTouchEvent
to dispatch to each ChildView for handling. If the ChildView consumes the event, it returns true, breaking the loop; otherwise, it queries the next ChildView.
// ViewGroup
// Start from this ViewGroup, passing down to the target of the click
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
// Perform an AND operation with the Mask to get the real Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Initialize FLAG_DISALLOW_INTERCEPT during Action down
final boolean intercepted;
// Check whether the ViewGroup can intercept the event
// Currently assume ViewGroup does not intercept
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
// Reorder the ChildViews in this ViewGroup
final ArrayList<View> preorderedList =
buildTouchDispatchChildList();
...
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
// Whether the animation can receive pointer events
// Whether the click is within the child element isTransformedTouchPointInView
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// Get the View being clicked
// New View events will return non-null
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Give it the new pointer in addition to the ones it is handling.
// Child View has received the click
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
// The key is in the dispatchTransformedTouchEvent method
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// ChildView successfully handles the event
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// addTouchTarget method
// will assign value to mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// Break the loop
break;
}
}
}
}
}
}
...
return handled;
}
ViewGroup#dispatchTransformedTouchEvent
method: ViewGroup will use this method to pass the event to ChildView. If the passed child is null, it calls the ViewGroup's parent dispatchTouchEvent; if it is not null, it calls the specified View's dispatchTouchEvent (in the current case, a View object is passed, so it will jump to the specified View#dispatchTouchEvent
method).
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event,
boolean cancel,
View child,
int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
// If the Child view is null, return to the ViewGroup's Parent
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
ViewGroup#addTouchTarget
: assigns the member mFirstTouchTarget
ViewGroup#addTouchTarget
: equips the member mFirstTouchTarget
// ViewGroup.java
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
// Get a reusable TouchTarget object
final TouchTarget target = TouchTarget
.obtain(child, pointerIdBits);
// Link to the previous event's View
target.next = mFirstTouchTarget;
// The first View that handles the click event
mFirstTouchTarget = target;
return target;
}
After the ChildView receives and intercepts the event (through the method), it will assign a value to ViewGroup#mFirstTouchTarget
and break the loop. After the ChildView processes the event, it returns to ViewGroup to continue processing.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
if (mFirstTouchTarget == null) {
...
} else {
...
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// Dispatch the event
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;
}
}
}
...
return handled;
}
ViewGroup Handling - ChildView Does Not Intercept ACTION_DOWN
#
ViewGroup ChildView intercepts the event: the handling method is the same (as in the previous section), here we mainly look at the different parts. This event is handled by the ViewGroup itself (dispatchTransformedTouchEvent method).
// ViewGroup.java
// Start from ViewGroup
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Execute reset only during Action Down
cancelAndClearTouchTargets(ev);
resetTouchState();
}
}
...
return handled;
}
Clear previous events: Note the resetTouchState method, which will be executed in ViewGroup#dispatchTouchEvent
when the event is ACTION_DOWN
. Whether to call ViewGroup#onInterceptTouchEvent
method: There are two conditions, plus a FLAG check: currently is event ACTION_DOWN
, and a ChildView has already handled this click event (if a child View handles the event, it will assign a value to mFirstTouchTarget
), whether the current ViewGroup is prohibited from intercepting (generally when ViewGroup receives an event, if it is not prohibited from intercepting, it will execute the onInterceptTouchEvent method) ACTION_DOWN
. If the ViewGroup itself does not intercept, it will recursively call ChildView: executing ViewGroup#dispatchTransformedTouchEvent
to dispatch to each ChildView for handling.
// ViewGroup
// Start from ViewGroup
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
if (!canceled && !intercepted) {
// Currently, there are no ChildViews to handle this event
}
}
...
}
ChildView receives but does not intercept the event, so ViewGroup#mFirstTouchTarget
is null.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
...
// Currently, there are no ChildViews handling the event
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
}
...
return handled;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
// Return to the ViewGroup's Parent dispatchTouchEvent
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
}
...
return handled;
}
View Receives Events - Handling ACTION_UP
#
The View first dispatches to onTouch; if it is not handled, it dispatches to onTouchEvent. The order is as follows:
- First executes the onTouch interface; if it has been handled, the onTouchEvent will not receive the event
onTouch
- If onTouch does not handle this event, it will be the turn of the View's onTouchEvent to handle the event
onTouchEvent
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
...
// Dispatch the event to onTouch
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
// Check if the View is enabled
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// Analyze onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
In the View#onTouchEvent
method, you can see the calls to onClick and onLongClick interfaces.
// View.java
// PerformClick represents the click event of this View
private PerformClick mPerformClick;
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// As long as one of CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE is set,
// this View is clickable
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// Click event proxy TouchDelegate
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
// Long press task mHasPerformedLongPress
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// Remove long press Callback
removeLongPressCallback();
// Only perform click actions if we were in the pressed state
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// Analyze performClickInternal
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
}
}
}
Here you can see that the View's click event is sent through a Handler for the Click task (Runnable), so it will not block the click event.
// View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// Place the Click task into the Handler
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
// Analyze performClick
return performClick();
}
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// Execute OnClick
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
ViewGroup - ACTION_DOWN#
- Determine whether the event is intercepted by ViewGroup:
If ViewGroup intercepts the event -> directly go to3
If ViewGroup does not intercept the event -> first go to2
, then3
- If ViewGroup does not intercept and one of the ChildViews intercepts the event and processes it
ViewGroup#newTouchTarget
is assigned a value
Dispatch to its own View (calls its parent)super.dispatchTouchEvent
// ViewGroup.java
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
- Event dispatch and handling: there are two situations
All ChildViews do not intercept this event, so mFirstTouchTarget is null, equivalent to the last View.
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
If a ChildView intercepts, then mFirstTouchTarget is not null, and the while loop only runs once because the ChildView has handled it (handled during dispatch).
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
ACTION_MOVE - View Received MOVE#
The Move event will still first go through the DecorView, this ViewGroup. When executing the event, it is necessary to know a few things about ACTION_MOVE
. The ViewGroup does not clear the click Flag, the TouchTarget type mFirstTouchTarget
element is not null (because a ChildView element has already processed), the judgment flow chart of the ACTION_MOVE event is as follows. It mainly checks whether the ViewGroup intercepts the event (the following assumes not intercepting). It will not enter the reset stage.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
...
}
}
Check whether the current ViewGroup prohibits interception; if not, call its own method to check for interception FLAG_DISALLOW_INTERCEPT
onInterceptTouchEvent
.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// Check whether ViewGroup is prohibited from intercepting
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// Assume ViewGroup does not intercept
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
It is not an event, so it will not enter the initial event dispatch stage ACTION_DOWN
.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// Not a Down event
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
}
}
...
}
The key point of ACTION_MOVE
is the event dispatch.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (mFirstTouchTarget == null) {
...
} else {
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// alreadyDispatchedToNewTouchTarget is false
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// Analyze dispatchTransformedTouchEvent to dispatch to ChildView
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
predecessor = target;
target = next;
}
}
}
Next, through the dispatchTransformedTouchEvent method: dispatch the event to ChildView's dispatchTouchEvent ACTION_MOVE
.
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits)
...
if (child == null) {
...
} 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);
}
}
ACTION_MOVE - ViewGroup Intercepts MOVE#
First, understand the current situation: a ChildView has already processed the ACTION_DOWN event, and we customize a ViewGroup and copy the ViewGroup#onInterceptTouchEvent
method to return true during ACTION_MOVE
to intercept.
// Custom ViewGroup.java
public class MyViewGroup extends View {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_MOVE) {
// ViewGroup consumes the event itself
return true;
}
return super.onInterceptTouchEvent(ev);
}
}
The ACTION_MOVE event occurs multiple times. The first ACTION_MOVE (coming from the ParentView) aims to:
- Cancel the ChildView event (change the event to ACTION_CANCEL)
- Set
ViewGroup#mFirstTouchTarget
to null; at this point, the ParentView does not handle the event.
As a result, the ChildView will receive an ACTION_CANCEL event (the event is canceled, and thereafter, the ViewGroup will handle the event).
/**
* ViewGroup.java
*
* Since overriding the onInterceptTouchEvent method makes the Move event true,
* cancelChild will also be true,
* the cancel parameter passed to the dispatchTransformedTouchEvent method will be true,
*
* At this point, the event will be changed to ACTION_CANCEL, and the ChildView will receive the ACTION_CANCEL event
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (mFirstTouchTarget == null) {
...
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// The first time entering ACTION_MOVE
// intercepted is true, so cancelChild = true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// mFirstTouchTarget = next, next is null
// so mFirstTouchTarget = null
if (cancelChild) {
if (predecessor == null) {
// next is null, so mFirstTouchTarget is null
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
// cancel is true
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// Change the event to ACTION_CANCEL
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
...
} else {
// Dispatch the event to ChildView as cancel
// Triggered when the event is intercepted by the upper layer
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
return handled;
}
The second ACTION_MOVE (coming from the ParentView), since mFirstTouchTarget is null, the ParentView will not dispatch.
As a result, the ParentView will handle the event itself.
//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// In the first ACTION_MOVE, mFirstTouchTarget is set to null
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
...
} else {
intercepted = true;
}
// intercepted = true
if (!canceled && !intercepted) {
...
}
if (mFirstTouchTarget == null) {
// ChildView is null
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
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;
}
Event Conflict#
Why does a conflict occur? Because there can only be one event, if the responding component is not the desired View object, it can be called an event conflict. It can be handled in two ways:
- Internal interception method: requires
ChildView#dispatchTouchEvent
to handle & requires cooperation to modify the parent container (ParentView) onInterceptTouchEvent interception. - External interception method (more commonly used): only requires ParentView to handle.
Internal Interception - ChildView Handling#
In ChildView, use the requestDisallowInterceptTouchEvent
method to control the FLAG_DISALLOW_INTERCEPT
element, so that the ParentView will not dispatch the event downwards. The internal interception method is for the ChildView to request the ParentView not to intercept the event at the appropriate time using the method requestDisallowInterceptTouchEvent
.
public class MyView extends View {
int dispatchX, dispatchY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Request ParentView not to intercept
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - dispatchX;
int deltaY = y - dispatchY;
// Let the event be handled by ViewGroup
if(Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
dispatchX = x;
dispatchY = y;
return super.dispatchTouchEvent(event);
}
}
Override the ParentView's method to set it to not intercept the event during ACTION_DOWN, so that the event can be passed to ChildView in onInterceptTouchEvent
.
public class MyViewGroup extends ViewGroup {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN) {
// ViewGroup does not intercept
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
Details of requestDisallowInterceptTouchEvent
: Why requestDisallowInterceptTouchEvent
cannot control the ACTION_DOWN event.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
// See resetTouchState
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // mFirstTouchTarget != null
// Control FLAG_DISALLOW_INTERCEPT
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// If true, the ParentView will not dispatch downwards
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//
}
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
From the above, it can be seen that in ViewGroup during ACTION_DOWN, the resetTouchState() method is used, which clears this Flag, causing the ACTION_DOWN event to be dispatched. FLAG_DISALLOW_INTERCEPT
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// After clearing, FLAG_DISALLOW_INTERCEPT is empty
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// Determine ViewGroup#onInterceptTouchEvent
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
...
}
Note
In ViewGroup, override onInterceptTouchEvent
to let ViewGroup return false during ACTION_DOWN (not intercept), so that ACTION_DOWN can be dispatched and passed down to ChildView, while other actions will be intercepted.
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == ACTION_DOWN) {
return false;
}
return true;
}
External Interception - ParentView Handling#
Handling event interception by ParentView is simpler; just override ParentView#onInterceptTouchEvent
to decide which time (condition) to intercept the event.
public class MyExternalViewGroup extends ViewGroup {
int interceptX, interceptY;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
boolean defaultIntercept = super.onInterceptTouchEvent(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - interceptX;
int deltaY = y - interceptY;
// Horizontal movement, intercept
if(Math.abs(deltaX) > Math.abs(deltaY)) {
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
default:
intercept = defaultIntercept;
break;
}
interceptX = x;
interceptY = y;
return intercept;
}
}