SoFunction
Updated on 2025-04-04

The implementation principle of android event distribution mechanism

Event processing in android and solving sliding conflict problems are inseparable from the event distribution mechanism. Event streams in android, namely MotionEvent, will go through a process from distribution, interception to processing. That is, a process from dispatchTouchEvent(), onInterceptEvent() to onTouchEvent(). DispatchTouchEvent() is responsible for the event distribution process. OnInterceptEvent() and onTouchEvent() will be called in dispatchTouchEvent(). If onInterceptEvent() returns true, the onTouchEvent() method of the current view will be called. If it is not intercepted, the event will be sent to dispatchTouchEvent() of the child view for the same operation. This article will lead you to analyze how android performs event distribution from the source code perspective.

The event distribution process in android starts with the dispatchTouchEvent() of the activity:

public boolean dispatchTouchEvent(MotionEvent ev) {
  if (() == MotionEvent.ACTION_DOWN) {
    onUserInteraction();
  }
  if (getWidow().superDispatchTouchEvent(ev)) {
    return true;
  }
  return onTouchEvent(ev);
}

GetWindow().superDispatchTouchEvent(ev) is called here. It can be seen here that activity passes MotionEvent to Window. Window is an abstract class, and superDispatchTouchEvent() is also an abstract method. Here we use the subclass of window phoneWindow.

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
  return (event);
} 

From here we can see that the event event was transmitted to the DecorView, which is our top view. We continue to track:

public boolean superDispatchTouchEvent(MotionEvent event) {
  return (event);
}

Here the dispatchTouchEvent() method of the parent class is called, and the DecorView is inherited from FrameLayout. FrameLayout inherits ViewGroup, so the dispatchTouchEvent() method of ViewGroup will be called here.

So the entire event stream starts from activity, is passed to the window, and finally to our view (viewGroup is also inherited from view), and the view is the core stage of our entire event processing.

Let's take a look at the implementation in the dispatchTouchEvent() of viewGroup:

if (actionMasked == MotionEvent.ACTION_DOWN) {
      // Throw away all previous state when starting a new touch gesture.
      // The framework may have dropped the up or cancel event for the previous gesture
      // due to an app switch, ANR, or some other state change.
      cancelAndClearTouchTargets(ev);
      resetTouchState();
    }

This is a piece of code intercepted at the beginning of dispatchTouchEvent(). Let's take a look. First, when we press the view with our finger, the resetTouchState() method will be called, in resetTouchState():

private void resetTouchState() {
  clearTouchTargets();
  resetCancelNextUpFlag(this);
  mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
  mNestedScrollAxes = SCROLL_AXIS_NONE;
}

We continue to track the clearTouchTargets() method:

private void clearTouchTargets() {
  TouchTarget target = mFirstTouchTarget;
  if (target != null) {
    do {
      TouchTarget next = ;
      ();
      target = next;
    } while (target != null);
    mFirstTouchTarget = null;
  }
}

In the clearTouchTargets() method, we finally assign mFirstTouchTarget to null, we continue to return to dispatchTouchEvent(), and then execute the following code:

// Check for interception.
final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
      final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
      if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        (action); // restore action in case it was changed
      } else {
        intercepted = false;
      }
    } else {
      // There are no touch targets and this action is not an initial down
      // so this view group continues to intercept touches.
      intercepted = true;
    }

When the view is pressed or mFirstTouchTarget != null, we can know from the previous step that when each view is pressed, that is, when the event stream is processed again, mFirstTouchTarget will be set to null. We will see when mFirstTouchTarget is assigned.

From the disallowIntercept property, we can roughly guess it is used to determine whether the intercept process is required. We know that by calling the requestDisallowInterceptTouchEvent(true) of the parent view, our parent view cannot intercept events. Let's first look at the implementation in the requestDisallowInterceptTouchEvent() method:

@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) {
    (disallowIntercept);
  }
}

This is also a judgment process by setting the flag bit, so here is to change the mGroupFlags flag, and then change the value of disallowIntercept in the dispatchTouchEvent() just sent. When it is true, it needs to be intercepted. At this time, the onInterceptTouchEvent() intercept judgment will be skipped and marked as not intercepted, that is, intercepted = false. Let's continue to look at the onInterceptTouchEvent() processing of viewGroup:

public boolean onInterceptTouchEvent(MotionEvent ev) {
  if ((InputDevice.SOURCE_MOUSE)
      && () == MotionEvent.ACTION_DOWN
      && (MotionEvent.BUTTON_PRIMARY)
      && isOnScrollbarThumb((), ())) {
    return true;
  }
  return false;
}

That is, by default, viewGroup will only behave as an interceptor when ACTION_DOWN.

Let's continue to read:

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
   final float x = (actionIndex);
   final float y = (actionIndex);
   // Find a child that can receive the event.
   // Scan children from front to back.
   final ArrayList<View> preorderedList = buildTouchDispatchChildList();
   final boolean customOrder = preorderedList == null
              && isChildrenDrawingOrderEnabled();
   final View[] children = mChildren;
   for (int i = childrenCount - 1; i >= 0; i--) {
      final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

            // If there is a view that has accessibility focus we want it
            // to get the event first and if not handled we will perform a
            // normal dispatch. We may do a double iteration but this is
            // safer given the timeframe.
            if (childWithAccessibilityFocus != null) {
              if (childWithAccessibilityFocus != child) {
                continue;
              }
              childWithAccessibilityFocus = null;
              i = childrenCount - 1;
            }

            if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
              (false);
              continue;
            }

            newTouchTarget = getTouchTarget(child);
            if (newTouchTarget != null) {
              // Child is already receiving touch within its bounds.
              // Give it the new pointer in addition to the ones it is handling.
               |= idBitsToAssign;
              break;
            }

            resetCancelNextUpFlag(child);
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
              // Child wants to receive touch within its bounds.
              mLastTouchDownTime = ();
              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;
              }
              mLastTouchDownX = ();
              mLastTouchDownY = ();
              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.
            (false);
          }
          if (preorderedList != null) ();
        }

This code will first traverse all subviews through a loop, and will eventually call the dispatchTransformedTouchEvent() method. Let's continue to look at the implementation of dispatchTransformedTouchEvent():

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits) {
  final boolean handled;

  // Canceling motions is a special case. We don't need to perform any transformations
  // or filtering. The important part is the action, not the contents.
  final int oldAction = ();
  if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    (MotionEvent.ACTION_CANCEL);
    if (child == null) {
      handled = (event);
    } else {
      handled = (event);
    }
    (oldAction);
    return handled;
  }

  // Calculate the number of pointers to deliver.
  final int oldPointerIdBits = ();
  final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

  // If for some reason we ended up in an inconsistent state where it looks like we
  // might produce a motion event with no pointers in it, then drop the event.
  if (newPointerIdBits == 0) {
    return false;
  }

  // If the number of pointers is the same and we don't need to perform any fancy
  // irreversible transformations, then we can reuse the motion event for this
  // dispatch as long as we are careful to revert any changes we make.
  // Otherwise we need to make a copy.
  final MotionEvent transformedEvent;
  if (newPointerIdBits == oldPointerIdBits) {
    if (child == null || ()) {
      if (child == null) {
        handled = (event);
      } else {
        final float offsetX = mScrollX - ;
        final float offsetY = mScrollY - ;
        (offsetX, offsetY);

        handled = (event);

        (-offsetX, -offsetY);
      }
      return handled;
    }
    transformedEvent = (event);
  } else {
    transformedEvent = (newPointerIdBits);
  }

  // Perform any necessary transformations and dispatch.
  if (child == null) {
    handled = (transformedEvent);
  } else {
    final float offsetX = mScrollX - ;
    final float offsetY = mScrollY - ;
    (offsetX, offsetY);
    if (! ()) {
      (());
    }

    handled = (transformedEvent);
  }

  // Done.
  ();
  return handled;
}

This code is more obvious. If child is not null, it will always be called (); otherwise, call ();

If child is not null, the event will be passed down. If the child view handles the event, dispatchTransformedTouchEvent() returns true. Continue to execute downward to the addTouchTarget() method, and we continue to look at the execution results of the addTouchTarget() method:

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
  final TouchTarget target = (child, pointerIdBits);
   = mFirstTouchTarget;
  mFirstTouchTarget = target;
  return target;
}

At this time, we found that mFirstTouchTarget appears again, and then the mFirstTouchTarget will be reassigned, that is, mFirstTouchTarget is not null. That is, if the event is consumed by the current view or subview, then mFirstTouchTarget is not null in the next ACTION_MOVE or ACTION_UP event. But if we inherit the viewGroup and intercept the event in the ACTION_MOVE of onInterceptTouchEvent(), then the subsequent events will not be issued and will be processed directly by the viewGroup. From the following code, we can get:

// Dispatch to touch targets, excluding the new touch target if we already
      // dispatched to it. Cancel touch targets if necessary.
      TouchTarget predecessor = null;
      TouchTarget target = mFirstTouchTarget;
      while (target != null) {
        final TouchTarget next = ;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
          handled = true;
        } else {
          final boolean cancelChild = resetCancelNextUpFlag()
              || intercepted;
          if (dispatchTransformedTouchEvent(ev, cancelChild,
              , )) {
            handled = true;
          }
          if (cancelChild) {
            if (predecessor == null) {
              mFirstTouchTarget = next;
            } else {
               = next;
            }
            ();
            target = next;
            continue;
          }
        }
        predecessor = target;
        target = next;
      }

When a child view exists and the event is consumed by a child view, that is, mFirstTouchTarget will be assigned in the ACTION_DOWN stage. That is, in the next ACTION_MOVE event, since intercepted is true, the ACTION_CANCEL event is passed, and you can see from dispatchTransformedTouchEvent():

if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    (MotionEvent.ACTION_CANCEL);
    if (child == null) {
      handled = (event);
    } else {
      handled = (event);
    }
    (oldAction);
    return handled;
  }

And finally assign mFirstTouchTarget to next, and at this time mFirstTouchTarget is located at the end of the TouchTarget link list, so mFirstTouchTarget will be assigned to null, and the next event will not enter onInterceptTouchEvent(). It will also be directly handed over to the view for processing.

If we do not intercept the event, but hand it over to the subview for processing, since the ViewGroup's onInterceptTouchEvent() will not intercept events other than ACTION_DOWN by default, subsequent events will continue to be handed over to the subview for processing, if there is a subview and the event is located in the inner area of ​​the subview.

So whether intercepting is performed or not, the event stream will be processed by the dispatchTouchEvent() of the view. Let's follow the processing process of dispatchTouchEvent() in the view:

if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Defensive cleanup for new gesture
    stopNestedScroll();
  }

  if (onFilterTouchEventForSecurity(event)) {
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
      result = true;
    }
    //noinspection SimplifiableIfStatement
    ListenerInfo li = mListenerInfo;
    if (li != null &&  != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && (this, event)) {
      result = true;
    }

    if (!result && onTouchEvent(event)) {
      result = true;
    }
  }

When pressed, that is, ACTION_DOWN, the view will stop scrolling internally. If the view is not overwritten or blocked, first, it will judge whether mListenerInfo is empty. Let's see where mListenerInfo is initialized:

ListenerInfo getListenerInfo() {
  if (mListenerInfo != null) {
    return mListenerInfo;
  }
  mListenerInfo = new ListenerInfo();
  return mListenerInfo;
}

It can be seen here that mListenerInfo is generally not null. We know that this code has been called when we use it. When the view is added to the window, the following code will be called, which can also be seen from the comments:

/**
 * Add a listener for attach state changes.
 *
 * This listener will be called whenever this view is attached or detached
 * from a window. Remove the listener using
 * {@link #removeOnAttachStateChangeListener(OnAttachStateChangeListener)}.
 *
 * @param listener Listener to attach
 * @see #removeOnAttachStateChangeListener(OnAttachStateChangeListener)
 */
public void addOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
  ListenerInfo li = getListenerInfo();
  if ( == null) {
    
        = new CopyOnWriteArrayList<OnAttachStateChangeListener>();
  }
  (listener);
}

At this point, we know that mListenerInfo was initialized from the beginning, so li cannot be null. != null means that when TouchListener is set, it is not null, and the view is enabled. Generally, the view is enabled. At this time, the onTouch() event will be called. When onTouch() returns true, the result will assign true value. And when result is true, onTouchEvent() will not be called.

From here, we can see that onTouch() will give priority to onTouchEvent() calls;
When the view sets touch listener and returns true, its onTouchEvent() will be blocked. Otherwise, onTouchEvent() will be called to handle it.

So let's continue to look at the event handling in onTouchEvent():

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

First of all, when the view status is DISABLED, as long as the view is CLICKABLE or LONG_CLICKABLE or CONTEXT_CLICKABLE, it will return true, and the button is CLICKABLE by default, the textview is not CLICKABLE by default, and the view is generally not LONG_CLICKABLE by default.

Let's continue to look down:

if (mTouchDelegate != null) {
    if ((event)) {
      return true;
    }
  }

If there is a proxy event, it will still 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,
                ());
          } else if (!post(mUnsetPressedState)) {
            // If the post failed, unpress right now
            ();
          }

          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();
          }
           = ();
           = ();
          postDelayed(mPendingCheckForTap, ());
        } else {
          // Not inside a scrolling container, so show the feedback right away
          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
        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;
  }

When the view is in the CLICKABLE or LONG_CLICKABLE or CONTEXT_CLICKABLE state, if click listening is set when the finger is raised, performClick() will eventually be called, triggering the click() event. This can be seen from the performClick() method:

public boolean performClick() {
  final boolean result;
  final ListenerInfo li = mListenerInfo;
  if (li != null &&  != null) {
    playSoundEffect();
    (this);
    result = true;
  } else {
    result = false;
  }

  sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  return result;
}

From here we can also figure out that the click event will be called in onTouchEvent(). If the view is set to listen onTouch() and returns true, the click event will also be blocked. However, we can continue to execute the click() event by calling the view's performClick() in onTouch(). This depends on the needs in our business.

From here we can see that if the event is not processed by the current view or subview, that is, false is returned, then the event will be handed over to the outer view to continue processing until it is consumed.

If the event has not been processed, it will eventually be passed to the onTouchEvent() of the Activity.

Let's summarize here:

Events are a pass process from Activity->Window->View(ViewGroup);

If the event is not intercepted midway, it will pass all the way to the innermost view control;

If the event is intercepted by a certain layer, the event will not be passed down and handed over to the view. If the view consumes an event, the next event will also be handed over to the view; if the view does not consume the event, the event will be handed over to the outer view, and finally called to the onTouchEvent() of the activity, unless a certain layer consumes the event;

An event can only be handled by one view;

DispatchTouchEvent() will always be called, and it will be called first, onInterceptTouchEvent() and onTouchEvent() are called internally within DispatchTouchEvent();

The sub-view cannot interfere with the ViewGroup's handling of the ACTION_DOWN event;

The child view can control the parent view not to intercept events through requestDisallowInterceptTouchEvent(true) and skip the execution of the onInterceptTouchEvent() method.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.