Detailed Explanation of Android Event Distribution Mechanism

  • 2021-07-26 08:47:45
  • OfStack

Before analyzing the event distribution mechanism of Android, two basic control types of android are defined: View and ViewGroup. View is an ordinary control without sub-layout, such as Button and TextView. ViewGroup inherits from View, indicating that there can be sub-controls, such as Linearlayout and Listview. Today, let's first understand the event distribution mechanism of View.
Let's look at the code first. It's very simple. There is only one Button, which is registered with the click events of OnClick and OnTouch respectively.


btn.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Log.i("Tag", "This is button onClick event");
      }
    });
    btn.setOnTouchListener(new View.OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
        Log.i("Tag", "This is button onTouch action" + event.getAction());
        return false;
      }
    });

Run the project under 1, and the result is as follows:
I/Tag: This is button onTouch action0
I/Tag: This is button onTouch action2
I/Tag: This is button onTouch action2
I/Tag: This is button onTouch action1
I/Tag: This is button onClick event
As you can see, onTouch is executed before onClick, so the order of event delivery is onTouch before OnClick. Specific why this is the case, the following will be explained by the source code. At this point, we may have noticed that the method of onTouch has a return value, and here it returns false. We changed it to true and ran it again, and the result is as follows:
I/Tag: This is button onTouch action0
I/Tag: This is button onTouch action2
I/Tag: This is button onTouch action2
I/Tag: This is button onTouch action2
I/Tag: This is button onTouch action1

Comparing the two results, we found that the onClick method is no longer implemented. Why is this so? I will clarify this idea step by step through the source code.
When looking at the source code, we should first know that all View type control event entries are dispatchTouchEvent (), so we directly enter the dispatchTouchEvent () method in the View class to see 1.


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();
    }
    if (onFilterTouchEventForSecurity(event)) {
      //noinspection SimplifiableIfStatement
      ListenerInfo li = mListenerInfo;
      if (li != null && li.mOnTouchListener != null
          && (mViewFlags & ENABLED_MASK) == ENABLED
          && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
      }
      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;
  }

As you can see from line 25 of the source code, the method of mOnTouchListener. onTouch () is executed first, if li! = null & & li.mOnTouchListener != null & & (mViewFlags & ENABLED_MASK) == ENABLED & & If li. mOnTouchListener. onTouch (this, event) are true, result is assigned to true, otherwise the onTouchEvent (event) method is executed.

It can be seen from the above that there are 4 conditions to meet,
1. ListenerInfo li, which is a static class in view, defines view event monitoring, etc. Therefore, ListenerInfo will be instantiated if there are view events, so li is not null
2. mOnTouchiListener is assigned in setOnTouchListener method. As long as touch event is registered, mOnTouchiListener1 will not null
3. (mViewFlags) & ENABLED_MASK) = = ENABLED is used to judge whether the currently clicked control is enable, button defaults to enable, and this condition is also constant to true.
4. Here's the point. li. mOnTouchListener. onTouch (this, event) is the callback control onTouch method. When this condition is also true, result=true, onTouchEvent (event) will not be executed. If onTouch returns false, the onTouchEvent (event) method is executed again.
We then go to the onTouchEvent method to view the source code.


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 ((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 (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
        (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
      switch (action) {
        case MotionEvent.ACTION_UP:
          boolean prepressed = (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 (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
              // This is a tap, so remove the longpress check
              removeLongPressCallback();
              // 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();
                }
                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;
          }
          // 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.
          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
            setPressed(true, x, y);
            checkForLongClick(0);
          }
          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;
  }

From the 21 lines of the source code, we can see that the control can be clicked to enter switch judgment, and when we trigger the fact that the finger leaves, it will enter MotionEvent.ACTION_UP, which is case. Let's move on to the mPerformClick () method called on line 50 of the source code and move on to the source code for this method.


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;
  }

Now we can see that as long as ListenerInfo and mOnClickListener are not null, the method onClick will be called. As mentioned before, as long as there is a listening event, ListenerInfo will not be null. Where is mOnClickListener assigned? Let's continue to look at its source code.


public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
      setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
  }

As you can see here, when we call the setOnClickListener method to register a click event for the button, we will assign a value to mOnClickListener. The whole order of distribution events is onTouch ()-- > onTouchEvent(event)-- > performClick()-- > OnClick ().
Now we can solve the previous problems.
1. The onTouch method takes precedence over OnClick, so onTouch is executed, and then onClick is executed.
2. Whether it is dispatchTouchEvent or onTouchEvent, if true is returned, it means that this event has been consumed, processed and no longer passed down. As you can see in the source code of dispathTouchEvent, if onTouchEvent returns true, it also returns true. If onTouch returns true while dispatchTouchEvent is listening to onTouch, it also returns true, which is consumed by onTouch in advance. onTouchEvent is no longer performed, let alone onClick monitoring.


Related articles: