Following the introduction of the previous Android event distribution mechanism source guide (two) – ViewGroup, We know how events go from the Activity’s dispatchTouchEvent to the top-level ViewDecorView to the ViewGroup’s dispatchTouchEvent, and the distribution of the ViewGroup layer, which I personally think is the most critical part of the whole event distribution, Understanding the event passing of the ViewGroup layer is equivalent to delivering the entire event. Now events are passed to the View layer. In this article, we will analyze the sequence of events in the View layer through the dispatchTouchEvent, onTouchEvent, OnTouchListener, and OnClickListener methods. First, let’s look at the dispatchTouchEvent method.
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { // If the event should be handled by accessibility focus first. if (event.isTargetAccessibilityFocus()) { // We don't have focus or no virtual descendant has it, do not handle the event. if (! isAccessibilityFocusedViewOrHost()) { return false; } // We have focus and got the event, then use normal event dispatch. event.setTargetAccessibilityFocus(false); } boolean result = false; if (mInputEventConsistencyVerifier ! = null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } / / in the same way as ViewGroup layer, security strategies are the if (onFilterTouchEventForSecurity (event)) {/ / mouse drag handle, If ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true; } // NoInspection SimplifiableIfStatement // an inner class containing various touch listeners ListenerInfo li = mListenerInfo; // Check if mOnTouchListener is empty. If mOnTouchListener is empty, skip it. If not, go to onTouchEvent. = null && li.mOnTouchListener ! = null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } //result is false if (! result && onTouchEvent(event)) { result = true; } } if (! result && mInputEventConsistencyVerifier ! = null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && ! result)) { stopNestedScroll(); } return result; }Copy the code
The View’s dispatchTouchEvent method is relatively simple and does not have complex logic. If the View is set to OnTouchListener and returns true, the result will be true. This directly causes the event to end distribution without entering the onTouchEvent method. If you don’t set the OnTouchListener, then it goes to the onTouchEvent method, and then the View’s dispatchTouchEvent method ends there. Next, let’s look at the onTouchEvent() method.
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
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 state of the View is DISABLED, the return value is determined by the View's clickability (single point, long press, etc.)
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if(action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) ! =0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if(mTouchDelegate ! =null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true; }}// If the View is clickable, return true, the event is consumed by default. Otherwise return false;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
// The ACTION_DOWN option sets the status of mPrivateFlags
booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! =0;
if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if(isFocusable() && isFocusableInTouchMode() && ! isFocused()) { focusTaken = requestFocus(); }if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
// If the event starts with ACTION_DOWN, mHasPerformedLongPress should be false
if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) {// This is a tap, so remove the longpress check
removeLongPressCallback();// Remove the long-press callback
// Only perform take click actions if we were in the pressed state
if(! focusTaken) {// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// Execute the click event
if(! post(mPerformClick)) { performClick(); }}}if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
// Cancel the Press state of the View
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:
// Set the trigger position label of the long press to false
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
// In a sliding container
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
// Set the View to Pressed and trigger the long press event
setPressed(true, x, y);
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
// Whether the position moved is no longer in the scope of the View
if(! pointInView(x, y, mTouchSlop)) {// Outside button
/ / remove CheckForTap
removeTapCallback();
if((mPrivateFlags & PFLAG_PRESSED) ! =0) {
// Remove any future long press/tap checks
// Remove the long-press task
removeLongPressCallback();
// Cancel the Press state of the View
setPressed(false); }}break;
}
return true;
}
return false;
}
Copy the code
Wow, at first glance this method is very long, thought very troublesome. In fact, it is very simple, is still no trouble logic. The above methods can be summarized into the following two points
1. Return true if View is clickable; Otherwise return false;
2. If the View is clickable and long pressed, the performLongClick() event is prefered, and when lifted, the onClick() event is triggered (provided you have OnClickListener set).
In addition to the above premise, the remaining point of the whole method is to analyze the processing in ACTION_DOWN, ACTION_MOVE, and ACTION_UP. So let’s first look at what’s going on in ACTION_DOWN.
I see this method on line 110 isInScrollingContainer, so let’s go in and see what we’re doing.
/ * * * @hideHidden methods */
public boolean isInScrollingContainer() {
ViewParent p = getParent();
while(p ! =null && p instanceof ViewGroup) {
if (((ViewGroup) p).shouldDelayChildPressedState()) {
return true;
}
p = p.getParent();
}
return false;
}Copy the code
This method is to call his father layout shouldDelayChildPressedState method, it could look at this method do
/**
* Return true if the pressed state should be delayed for children or descendants of this
* ViewGroup. Generally, this should be done for containers that can scroll, such as a List.
* This prevents the pressed state from appearing when the user is actually trying to scroll
* the content.
*
* The default implementation returns true for compatibility reasons. Subclasses that do
* not scroll should generally override this method and return false.
*/
public boolean shouldDelayChildPressedState() {
return true;
}Copy the code
This comment roughly means that the parent layout should return true if it is slidable; Otherwise, return False; This is not part of our discussion of the question, interested students can explore further.
The main purpose of this code is to delay the execution of the CheckForTap task. The main purpose of this code is to delay the CheckForTap task.
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
/ / change the flag
mPrivateFlags &= ~PFLAG_PREPRESSED;
// Set pressed to true
setPressed(true, x, y);
// Check and presscheckForLongClick(ViewConfiguration.getTapTimeout(), x, y); }}Copy the code
This is a class that implements the Runnable interface, and we’ll see later that many of the operations that do this implement the Runnable interface; This code is similar to the code at 128, which means that the difference between the two is whether the subsequent tasks are delayed. Ok, so let’s look at what this checkForLongClick method does.
private void checkForLongClick(int delayOffset, float x, float y) {
// Whether the View can be long pressed
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
// Instance long press task
mPendingCheckForLongPress = new CheckForLongPress();
}
// Set the anchor point
mPendingCheckForLongPress.setAnchor(x, y);
/ / modify WindowAttachCount
mPendingCheckForLongPress.rememberWindowAttachCount();
// Delay executing taskspostDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); }}Copy the code
This method is very simple, first check whether the View can be long pressed, if true, set the current position, and change the corresponding View associated with the number of Windows. Then delay the task.
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
@Override
public void run() {
//pressed is true and the parent layout is not empty
if(isPressed() && (mParent ! =null)
/ / there are basically call rememberWindowAttachCount this method is basically the same
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true; }}}public void setAnchor(float x, float y) {
mX = x;
mY = y;
}
public void rememberWindowAttachCount() { mOriginalWindowAttachCount = mWindowAttachCount; }}/**
* Calls this view's OnLongClickListener, if it is defined. Invokes the
* context menu if the OnLongClickListener did not consume the event,
* anchoring it to an (x,y) coordinate.
*
* @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
* to disable anchoring
* @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
* to disable anchoring
* @return {@code true} if one of the above receivers consumed the event,
* {@code false} otherwise
*/
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
/**
* Calls this view's OnLongClickListener, if it is defined. Invokes the
* context menu if the OnLongClickListener did not consume the event.
*
* @return {@code true} if one of the above receivers consumed the event,
* {@code false} otherwise
*/
public boolean performLongClick() {
return performLongClickInternal(mLongClickX, mLongClickY);
}
/**
* Calls this view's OnLongClickListener, if it is defined. Invokes the
* context menu if the OnLongClickListener did not consume the event,
* optionally anchoring it to an (x,y) coordinate.
*
* @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
* to disable anchoring
* @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
* to disable anchoring
* @return {@code true} if one of the above receivers consumed the event,
* {@code false} otherwise
*/
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
final ListenerInfo li = mListenerInfo;
// Trigger the long press listening event
if(li ! =null&& li.mOnLongClickListener ! =null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if(! handled) {final booleanisAnchored = ! Float.isNaN(x) && ! Float.isNaN(y); handled = isAnchored ? showContextMenu(x, y) : showContextMenu(); }if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}Copy the code
The above code does not have much logic, and the final implementation is the last piece. As you can see from the end here, the View’s onTouchEvent event handles the first ACTION_DOWN event, responds first to the long press listener (if you have it set), and has a corresponding delay if the layout is slideable (such as the listView). Okay, so that’s where the ACTION_DOWN event ends. Next, let’s look at the flow of the ACTION_MOVE event.
You get the basics from looking at the comments on lines 145-157 of the code above. Within the scope of View, basically nothing. So let’s look at what ACTION_UP does.
We can look directly at PerformClick on line 74. The rest of the code is basically a bunch of simple judgments, mainly the PerformClick class.
private final class PerformClick implements Runnable {
@Override
public void run() { performClick(); }}/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if(li ! =null&& li.mOnClickListener ! =null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}Copy the code
As we see here, the ACTION_UP event will eventually go to the OnClickListener listener. From there we can see that when the first ACTION_DOWN event comes down, at this level of the View the first is
OnTouchListener – > onTouchEvent – > OnLongClickListener – > an OnClickListener.
So let’s do a summary of the View layer event distribution.