What happens when your finger moves from button A to button B in Android? Why?

Author: TechMerger

Preface

Touch-related questions are frequently asked in Android interviews. Everyone is not necessarily required to answer from the bottom of InputFlinger, but at least they need to understand the complete processing of Touch after it reaches the App. Even this upper-level link should not be limited to a clichéd process retelling. It requires a deep understanding and flexible application of the details and principles.

This article combines a simple question and answer on a Touch scenario to help you deepen your understanding of Touch distribution.

  1. Buttons A and B are adjacent, and if you don’t lift your finger and move from A to B, what will happen to A? Why?
  2. At this moment, what will happen to B? Why?
  3. After that, what happens when the finger translates from B back to A? Why?
  4. Finally, if you lift your finger on A, will A trigger a click? Why?

verify

We customize two Buttons to overwrite them respectively onTouchEvent(), ConstraintLayoutplace them closely up and down in a , and set them to different background colors for differentiation.

Follow the steps in the question to start giving it a try.

You can see that the moment the finger moves to B, the press effect of A is gone, and B has no reaction. Even if it is moved back to A, A cannot restore the press effect, and click is not triggered after it is lifted.

answer

Before answering the principle, let's look at the log first and then explain it one by one.

 // 手指在 A 上按下
 2023-09-12 18:11:25.209 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_DOWN, actionButton=0, id[0]=0, x[0]=109.9668, y[0]=74.92432, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1823125, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=530500549 }
 ​
 // 手指开始向下移动
 2023-09-12 18:11:25.586 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=109.9668, y[0]=78.92334, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1823538, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=348888341 }
 2023-09-12 18:11:25.633 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=109.9668, y[0]=82.92236, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1823591, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=354173977 }
 ...
 2023-09-12 18:11:26.200 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=121.96387, y[0]=155.50244, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1824161, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=195296965 }
 2023-09-12 18:11:26.216 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=121.96387, y[0]=163.84363, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1824177, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=273686682 }
 ​
 // Button 高度为 168px,此刻已开始出界到 B
 2023-09-12 18:11:26.233 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=121.96387, y[0]=174.2472, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1824194, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=758026894 }
 2023-09-12 18:11:26.250 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=121.96387, y[0]=178.18982, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=1824211, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=498491454 }
 ...
 2023-09-12 18:11:26.801 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=129.96191, y[0]=266.87744, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1824754, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=936130601 }
 ​
 // 手指开始往上移动
 2023-09-12 18:11:27.484 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=129.96191, y[0]=262.87842, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1825443, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=17662257 }
 ...
 2023-09-12 18:11:27.585 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=137.95996, y[0]=244.88281, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1825541, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=507118427 }
 ...
 2023-09-12 18:11:27.966 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=145.16235, y[0]=175.69556, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1825927, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=876127266 }
 ​
 // Button 高度为 168px,此刻已移动回到 A
 2023-09-12 18:11:27.985 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=145.95801, y[0]=166.91626, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1825944, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=352798882 }
 2023-09-12 18:11:28.000 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=149.15863, y[0]=162.90283, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1825961, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=99105321 }
 ...
 2023-09-12 18:11:28.369 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=161.9541, y[0]=86.92139, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1826312, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=764248821 }
 2023-09-12 18:11:28.722 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=161.9541, y[0]=90.92041, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1826673, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=197617005 }
 ​
 // 手指从 A 上抬起
 2023-09-12 18:11:28.947 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_UP, actionButton=0, id[0]=0, x[0]=161.9541, y[0]=90.92041, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1826912, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=250168391 }

1. Translate to B, what happens to A?

A's pressed effect will be reset.

In the past, everyone would intuitively think that this was caused by ViewGroup sending ACTION_CANCEL to ButtonA.

But if you observe the log, you will find that even if it goes out of bounds, ACTION_MOVE is always sent to ButtonA. At the same time, as the finger continues to move downward, the y relative coordinate of ACTION_MOVE continues to increase. When the y value exceeds the height difference between mBottom - mTop, Button's parent View will call to refresh based on its leaving the View boundary onTouchEvent(). setPressed(false)The Press state of View then causes ButtonA's pressed state to disappear.

 public class View ... {
     ...
     public boolean onTouchEvent(MotionEvent event) {
         ...
         if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
             switch (action) {
                 ...
                 case MotionEvent.ACTION_MOVE:
                     ...
                     // Be lenient about moving outside of buttons
                     if (!pointInView(x, y, touchSlop)) {
                         ...
                         if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                             setPressed(false);
                         }
                         mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                     }
                     ...
                     break;
             }
 ​
             return true;
         }
 ​
         return false;
     }
 ​
     /*package*/ final boolean pointInView(float localX, float localY) {
         return pointInView(localX, localY, 0);
     }
 ​
     public boolean pointInView(float localX, float localY, float slop) {
         return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
                 localY < ((mBottom - mTop) + slop);
     }
     ...
 }

2. What will happen to B? Why?

B has no reaction.

In fact, when answering question 1, the direct reason for B's unresponsiveness has been answered: ButtonB did not receive any TouchEvent.

So why doesn't the system send the event even if the finger moves to area B?

Button's parent layout assigns the child that handles the DOWN event to mFirstTouchTargetViewGroup when distributing ACTION_DOWN . When ACTION_MOVE comes later, it is found that mFirstTouchTarget already exists, so the subsequent events will continue to be sent to it through .addTouchTarget()dispatchTransformedTouchEvent()TouchTarget

The comments in the source code also reflect this:

Dispatch to touch targets, excluding the new touch target if we already dispatched to it.

 public abstract class ViewGroup extends View implements ViewParent, ViewManager {
     ...
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         ...
         boolean handled = false;
         if (onFilterTouchEventForSecurity(ev)) {
             ...
             if (!canceled && !intercepted) {
                 ...
                 if (actionMasked == MotionEvent.ACTION_DOWN ...) {
                     ...
 ​
                     final int childrenCount = mChildrenCount;
                     if (newTouchTarget == null && childrenCount != 0) {
                         ...
                         final View[] children = mChildren;
                         for (int i = childrenCount - 1; i >= 0; i--) {
                             ...
                             resetCancelNextUpFlag(child);
                             if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                 // Child wants to receive touch within its bounds.
                                 mLastTouchDownTime = ev.getDownTime();
                                 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 = ev.getX();
                                 mLastTouchDownY = ev.getY();
                                 newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                 alreadyDispatchedToNewTouchTarget = true;
                                 break;
                             }
                             ...
                         }
                         ...
                     }
                     ...
                 }
             }
 ​
             if (mFirstTouchTarget == null) {
                 handled = dispatchTransformedTouchEvent(ev, canceled, null,
                         TouchTarget.ALL_POINTER_IDS);
             } 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;
                         if (dispatchTransformedTouchEvent(ev, cancelChild,
                                 target.child, target.pointerIdBits)) {
                             handled = true;
                         }
                         ...
                     }
                     predecessor = target;
                     target = next;
                 }
             }
             ...
         }
         ...
         return handled;
     }
     ...
 }

3. What happens after B is translated back to A?

A no longer reacts.

Button's parent View ACTION_DOWNcan only call when it receives to setPressed()display the pressed effect. So even if the finger returns to area A, it will not trigger the change of pressing UI.

 public class View ... {
     ...
     public boolean onTouchEvent(MotionEvent event) {
         ...
         if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
             switch (action) {
                 case MotionEvent.ACTION_UP:
                     mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                     if ((viewFlags & TOOLTIP) == TOOLTIP) {
                         handleTooltipUp();
                     }
                     if (!clickable) {
                         removeTapCallback();
                         removeLongPressCallback();
                         mInContextButtonPress = false;
                         mHasPerformedLongPress = false;
                         mIgnoreNextUpEvent = false;
                         break;
                     }
                     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)) {
                                     performClickInternal();
                                 }
                             }
                         }
 ​
                         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:
                     ...
                     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(
                                 ViewConfiguration.getLongPressTimeout(),
                                 x,
                                 y,
                                 TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                     }
                 ...
             }
 ​
             return true;
         }
 ​
         return false;
     }
 ​
     /*package*/ final boolean pointInView(float localX, float localY) {
         return pointInView(localX, localY, 0);
     }
 ​
     public boolean pointInView(float localX, float localY, float slop) {
         return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
                 localY < ((mBottom - mTop) + slop);
     }
     ...
 }

4. Will A trigger a click? Why?

Unable to trigger click.

The reason is simple. The moment it is removed from A, the execution performClickof Runnableis deleted, and then there is no chance to trigger click or longClick.

 public class View ... {
     ...
     public boolean onTouchEvent(MotionEvent event) {
         ...
         if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
             switch (action) {
                 ...
                 case MotionEvent.ACTION_MOVE:
                     ...
                     if (!pointInView(x, y, touchSlop)) {
                         // Outside button
                         // Remove any future long press/tap checks
                         removeTapCallback();
                         removeLongPressCallback();
                         ...
                     }
                     ...
                     break;
             }
 ​
             return true;
         }
 ​
         return false;
     }
     ...
 }

Conclusion

Review the answers and reasons for these 4 questions.

  1. Buttons A and B are adjacent, and if you don’t lift your finger and move from A to B, what will happen to A? Why?

    The pressing effect of A will disappear.

    Even if the finger moves out of bounds, the MOVE event is still sent to A. The View resets the pressed state after finding that the coordinates exceed the Button range.

  2. At this moment, what will happen to B? Why?

    B Nothing changes.

    Button A received the DOWN event first, causing subsequent events to be sent to A. Button B did not receive any events, so there was no response.

  3. After that, what happens when the finger translates from B back to A? Why?

    A also does not restore the pressing effect.

    View only sets the pressed state when receiving DOWN. Even if the finger returns to A, because no new DOWN is generated, the pressing effect cannot be displayed again.

  4. Finally, if you lift your finger on A, will A trigger a click? Why?

    A's click cannot be triggered.

    The moment the finger goes out of bounds from A, the click runnable is also removed. There is no runnable that can be executed when it goes up later, so no callbacks for clicks or long presses will be executed.

There is no doubt that Android has no problem doing this. So if we want to change this logic:

  1. How to make the moved target Button appear in pressed state and respond to click when the finger is lifted?

The idea is not complicated. Simply put, the copy ViewGroupof dispatchTouchEvent()can be processed as follows:

  1. It is found that the touchTarget has changed, and CANCEL is sent to the original target to cancel the pressed effect.
  2. Manually obtain a DOWN event and send it to the moved target, so that the new target can display the pressed state and set click runnable
  3. Then send the actual physical MOVE event to the new target. Later, when UP, because of DOWN, runnable is added to ensure that click can be executed when up.

That’s it for now. Have you answered these 5 questions correctly? I hope this article can help you deepen your understanding of Touch processing.

Android study notes

Android performance optimization article: Android Framework underlying principles article: Android vehicle article: Android reverse security study notes: Android audio and video article: Jetpack family bucket article (including Compose): OkHttp source code analysis notes: Kotlin article: Gradle article: Flutter article: Eight knowledge bodies of Android: Android core notes: Android interview questions from previous years: The latest Android interview questions in 2023: Android vehicle development position interview exercises: Audio and video interview questions:https://qr18.cn/FVlo89
https://qr18.cn/AQpN4J
https://qr18.cn/F05ZCM
https://qr18.cn/CQ5TcL
https://qr18.cn/Ei3VPD
https://qr18.cn/A0gajp
https://qr18.cn/Cw0pBD
https://qr18.cn/CdjtAF
https://qr18.cn/DzrmMB
https://qr18.cn/DIvKma
https://qr18.cn/CyxarU
https://qr21.cn/CaZQLo
https://qr18.cn/CKV8OZ
https://qr18.cn/CgxrRy
https://qr18.cn/FTlyCJ
https://qr18.cn/AcV6Ap

Guess you like

Origin blog.csdn.net/weixin_61845324/article/details/133029380