Event Passing#
Events are passed from Activity to View layer by layer.
Concept of Event Dispatching#
It is mainly accomplished by 3 methods.
Key Dispatch Method | Function | Return Meaning |
---|---|---|
Dispatch Touch Event: boolean | Triggered by the upper View, passed to the target View | The return result will be 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, starts 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
, and the subsequent direction is determined 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 this 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 understand how the click event is passed to DecorView#ViewGroup
. Here we will further analyze the ViewGroup click event. First, let's understand several MotionEvents:
MotionEvent Event | Action | Others |
---|---|---|
ACTION_DOWN | Finger down | Divided into intercept and not intercept |
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 the main tasks it performs are (the main handling items will be listed first):
Clearing previous events: Note the resetTouchState method, which will be executed when the ViewGroup#dispatchTouchEvent event is ACTION_DOWN.
Note
Clearing previous events: Note the resetTouchState method, which will be executed when the ViewGroup#dispatchTouchEvent event is ACTION_DOWN.
// ViewGroup.java
// Start from this ViewGroup, passing the click target down
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 Mask to get the actual 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, allow ViewGroup to intercept
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
// Clear all events in the first click 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;
}
}
Will the ViewGroup#onInterceptTouchEvent method be called? There will be 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 the ViewGroup receives an ACTION_DOWN, if it is not prohibited from intercepting, it will execute the onInterceptTouchEvent method).
// ViewGroup.java
// Start from this ViewGroup, passing the click target down
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 Mask to get the actual 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 the 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 if intercept 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 processing. If the ChildView consumes the event, it returns true, then exits the loop; otherwise, it queries the next ChildView.
// ViewGroup
// Start from this ViewGroup, passing the click target down
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 Mask to get the actual Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Initialize FLAG_DISALLOW_INTERCEPT during Action down
final boolean intercepted;
// Determine whether the ViewGroup can intercept the event
// Currently assuming the 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 animation can receive pointer events
// Whether the click is within the child element
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 focus is on the dispatchTransformedTouchEvent method
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// ChildView successfully handles the event
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// addTouchTarget method
// It will assign a value to mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// Exit the loop
break;
}
}
}
}
}
}
...
return handled;
}
ViewGroup#dispatchTransformedTouchEvent
method: The ViewGroup will use this method to pass the event to the ChildView. If the incoming child is null, it will call the ViewGroup's parent dispatchTouchEvent; if it is not null, it will call the specified View's dispatchTouchEvent (in this case, since a View object is passed in, 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 mFirstTouchTarget
member.
// 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 exit the loop. After the ChildView processes the event, it returns to the 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 events: 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;
}
Clearing previous events: Note the resetTouchState method, which will be executed when ViewGroup#dispatchTouchEvent
event is ACTION_DOWN
. Will the ViewGroup#onInterceptTouchEvent
method be called? There will be two conditions, plus a FLAG check: currently is the event ACTION_DOWN
, 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 the ViewGroup receives an ACTION_DOWN, if it is not prohibited from intercepting, it will execute the onInterceptTouchEvent method). If the ViewGroup itself does not intercept, it will recursively call ChildView: executing ViewGroup#dispatchTransformedTouchEvent
to dispatch to each ChildView for processing.
// ViewGroup
// Start from ViewGroup
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
if (!canceled && !intercepted) {
// Currently, there is no ChildView to handle this event
}
}
...
}
The ChildView receives but does not intercept the event, resulting in ViewGroup#mFirstTouchTarget
being null.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
...
// Currently, there is no ChildView 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, the onTouch interface is executed; if it has been handled, then 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 this 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 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, it can be seen that the View's click event is sent through a Handler for the Click task (Runnable), so that it does 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 the ViewGroup:
If the ViewGroup intercepts the event -> directly go to3
If the ViewGroup does not intercept the event -> first go to2
then3
- If the ViewGroup does not intercept, and one of the ChildViews intercepts the event and handles it
ViewGroup#newTouchTarget
is assigned a value
Dispatch to its own View (calls its own 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 dispatching 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, needs to know a few things about ACTION_MOVE
. The ViewGroup does not clear the click Flag, the TouchTarget type mFirstTouchTarget
element is not empty (because a ChildView element has already handled it), the judgment flow chart of the ACTION_MOVE event is as follows, its main task is to determine whether the ViewGroup intercepts the event (the following assumes it does not intercept), it does not intercept, Move does not dispatch events or handle them, similarly, let’s first observe the ViewGroup#dispatchTouchEvent method and start analyzing from here, it will not enter the reset stage.
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
...
}
}
Determine 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 if the ViewGroup prohibits interception
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// Preset that the 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;
}
}
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 focus of ACTION_MOVE
is on event dispatching.
// 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 the 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 handled 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 first ACTION_MOVE event (coming from the ParentView) aims to:
- Cancel ChildView events (the event changes 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 the ACTION_CANCEL event (the event is canceled, and thereafter will be handled by the ViewGroup).
/**
* ViewGroup.java
*
* Since overriding the onInterceptTouchEvent method will make 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 below 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
// The event is triggered when 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 empty, the ParentView will not dispatch it.
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 empty
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 is only one event, if the responding component is not the desired View original, it can be called an event conflict. There are two methods to handle it:
- Internal interception method: requires
ChildView#dispatchTouchEvent
handling & needs to cooperate with changes to the parent container (ParentView) onInterceptTouchEvent interception. - External interception method (more commonly used): only requires ParentView handling.
Internal Interception - ChildView Handling#
In the ChildView, use the requestDisallowInterceptTouchEvent
method to control the FLAG_DISALLOW_INTERCEPT
element, so that the ParentView will not dispatch the event down. The internal interception method requires 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 events during ACTION_DOWN, so that the events 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 down
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 the ViewGroup, during ACTION_DOWN, the resetTouchState() method is used, which clears this Flag, causing the ACTION_DOWN event to be dispatched.
// 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 the ViewGroup, override onInterceptTouchEvent
to make the ViewGroup return false during ACTION_DOWN (not intercept), so that ACTION_DOWN can be dispatched, allowing it to be 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 the ParentView is simpler; just override ParentView#onInterceptTouchEvent
to decide which time (conditions) 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;
}
}