Capture KEYCODE_DPAD_CENTER button in Android Activity

platform

    RK3568 + Android 11 + AndroidStuido
insert image description here

overview

    test code

public class KeybuttonTest extends Activity {
    
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.key_button_test);
  }


  @Override
  public boolean onKeyDown(int keyCode, KeyEvent event) {
    
    
    return super.onKeyDown(keyCode, event);
  }
  @Override
  public boolean onKeyUp(int keyCode, KeyEvent event) {
    
    
    return super.onKeyDown(keyCode, event);
  }
}

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/tvInfo"
        android:textSize="@dimen/fontMsg"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

Problem:
    Under normal circumstances, the Activity can capture the down and up events of the button. However, when the input button is KEYCODE_DPAD_CENTER and KEYCODE_ENTER , it only receives ACTION_UP once, and the subsequent onKeyDown and onKeyDown do not detect the button.

From the perspective of Android's event distribution mechanism, the preliminary judgment is that the key was captured by a certain control. In the layout, there is only one TextView control.

some try

  • Solution 1: Disable TextView focus capture
    findViewById(R.id.tvInfo).setFocusable(false);

The test function is normal, and the Activity can capture key events normally.

In subsequent schemes, the first KeyDown is used to handle control focus


  • Option Two: Capture Before Distribution
  @Override
  public boolean dispatchKeyEvent(KeyEvent event) {
    
    
    return super.dispatchKeyEvent(event);
  }


//控件获得了焦点
onFocusChange true
//只收到一个ACTION_UP
dispatchKeyEvent action(1),source(0x00000000),deviceId(-1),code(23),label(KEYCODE_DPAD_CENTER)


  • Solution 3: Rewrite the onKeyDown/onKeyUp of the View control
  public static class TV extends TextView {
    
    
    final String TAG = "TV";
    public TV(Context context) {
    
    
      super(context);
    }

    public TV(Context context, AttributeSet attrs) {
    
    
      super(context, attrs);
    }

    public TV(Context context, AttributeSet attrs, int defStyleAttr) {
    
    
      super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
    
    
      //new Exception("TV.onKeyDown").printStackTrace();
      boolean b = super.onKeyDown(keyCode, event);
      Logger.d(TAG, "onKeyDown " + b + ":" + keyCode);
      return b;
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
    
    
      boolean b =  super.onKeyUp(keyCode, event);
      Logger.d(TAG, "onKeyUp " + b + ":" + keyCode);
      return b;
    }
  }
//第一次触发
D/KeybuttonTest: ALog > onFocusChange true
D/KeybuttonTest: ALog > dispatchKeyEvent 23
D/TV: ALog > onKeyUp false:23
D/KeybuttonTest: ALog > onKeyUp false:23

//后续的触发
D/KeybuttonTest: ALog > dispatchKeyEvent 23
D/TV: ALog > onKeyDown true:23
D/KeybuttonTest: ALog > dispatchKeyEvent 23
D/KeybuttonTest: ALog > onClick
D/TV: ALog > onKeyUp true:23

If you add focus time monitoring to the control key, you can see that
when KEYCODE_DPAD_CENTER is pressed, whether there is focus processing is different

first current focus state state after pressing Whether to pass to Activity Whether to trigger OnClick
yes no focus have focus Only passed onKeyUp no
no have focus have focus not pass yes

analyze

first trigger focus

  1. After receiving the key, EarlyPostImeInputStage.processKeyEvent
  2. Determine whether to exit haunted mode: checkForLeavingTouchModeAndConsume
  3. It can be known from the isNavigationKey function that KEYCODE_DPAD_CENTER also belongs to the navigation key
  4. Follow all the way: ensureTouchMode -> ensureTouchModeLocally -> leaveTouchMode

frameworks/base/core/java/android/view/ViewRootImpl.java

	//...
    final class EarlyPostImeInputStage extends InputStage {
    
    
        private int processKeyEvent(QueuedInputEvent q) {
    
    
        	//...
            // If the key's purpose is to exit touch mode then we consume it
            // and consider it handled.
            if (checkForLeavingTouchModeAndConsume(event)) {
    
    
                return FINISH_HANDLED;
            }
        }
    }
    /**
     * See if the key event means we should leave touch mode (and leave touch mode if so).
     * @param event The key event.
     * @return Whether this key event should be consumed (meaning the act of
     *   leaving touch mode alone is considered the event).
     */
    private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
    
    
        // Only relevant in touch mode.
        if (!mAttachInfo.mInTouchMode) {
    
    
            return false;
        }

        // Only consider leaving touch mode on DOWN or MULTIPLE actions, never on UP.
        final int action = event.getAction();
        if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
    
    
            return false;
        }

        // Don't leave touch mode if the IME told us not to.
        if ((event.getFlags() & KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
    
    
            return false;
        }

        // If the key can be used for keyboard navigation then leave touch mode
        // and select a focused view if needed (in ensureTouchMode).
        // When a new focused view is selected, we consume the navigation key because
        // navigation doesn't make much sense unless a view already has focus so
        // the key's purpose is to set focus.
        if (isNavigationKey(event)) {
    
    
            return ensureTouchMode(false);
        }

        // If the key can be used for typing then leave touch mode
        // and select a focused view if needed (in ensureTouchMode).
        // Always allow the view to process the typing key.
        if (isTypingKey(event)) {
    
    
            ensureTouchMode(false);
            return false;
        }

        return false;
    }
    private static boolean isNavigationKey(KeyEvent keyEvent) {
    
    
        switch (keyEvent.getKeyCode()) {
    
    
        case KeyEvent.KEYCODE_DPAD_LEFT:
        case KeyEvent.KEYCODE_DPAD_RIGHT:
        case KeyEvent.KEYCODE_DPAD_UP:
        case KeyEvent.KEYCODE_DPAD_DOWN:
        case KeyEvent.KEYCODE_DPAD_CENTER:
        case KeyEvent.KEYCODE_PAGE_UP:
        case KeyEvent.KEYCODE_PAGE_DOWN:
        case KeyEvent.KEYCODE_MOVE_HOME:
        case KeyEvent.KEYCODE_MOVE_END:
        case KeyEvent.KEYCODE_TAB:
        case KeyEvent.KEYCODE_SPACE:
        case KeyEvent.KEYCODE_ENTER:
            return true;
        }
        return false;
    }
    
    private boolean leaveTouchMode() {
    
    
        if (mView != null) {
    
    
            if (mView.hasFocus()) {
    
    
                View focusedView = mView.findFocus();
                if (!(focusedView instanceof ViewGroup)) {
    
    
                    // some view has focus, let it keep it
                    return false;
                } else if (((ViewGroup) focusedView).getDescendantFocusability() !=
                        ViewGroup.FOCUS_AFTER_DESCENDANTS) {
    
    
                    // some view group has focus, and doesn't prefer its children
                    // over itself for focus, so let them keep it.
                    return false;
                }
            }

            // find the best view to give focus to in this brave new non-touch-mode
            // world
            return mView.restoreDefaultFocus();
        }
        return false;
    }

frameworks/base/core/java/android/view/ViewGroup.java

    @Override
    public boolean restoreDefaultFocus() {
    
    
        if (mDefaultFocus != null
                && getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS
                && (mDefaultFocus.mViewFlags & VISIBILITY_MASK) == VISIBLE
                && mDefaultFocus.restoreDefaultFocus()) {
    
    
            return true;
        }
        return super.restoreDefaultFocus();
    }

Finally View gets the focus:

frameworks/base/core/java/android/view/View.java

    /**
     * Gives focus to the default-focus view in the view hierarchy that has this view as a root.
     * If the default-focus view cannot be found, falls back to calling {@link #requestFocus(int)}.
     *
     * @return Whether this view or one of its descendants actually took focus
     */
    public boolean restoreDefaultFocus() {
    
    
        return requestFocus(View.FOCUS_DOWN);
    }
    public final boolean requestFocus(int direction) {
    
    
        return requestFocus(direction, null);
    }
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    
    
        return requestFocusNoSearch(direction, previouslyFocusedRect);
    }
    
    private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
    
    
        // need to be focusable
        if (!canTakeFocus()) {
    
    
            return false;
        }

        // need to be focusable in touch mode if in touch mode
        if (isInTouchMode() &&
            (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
    
    
               return false;
        }

        // need to not have any parents blocking us
        if (hasAncestorThatBlocksDescendantFocus()) {
    
    
            return false;
        }

        if (!isLayoutValid()) {
    
    
            mPrivateFlags |= PFLAG_WANTS_FOCUS;
        } else {
    
    
            clearParentsWantFocus();
        }

        handleFocusGainInternal(direction, previouslyFocusedRect);
        return true;
    }
    void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
    
    
        if (DBG) {
    
    
            System.out.println(this + " requestFocus()");
        }

        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
    
    
            mPrivateFlags |= PFLAG_FOCUSED;

            View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

            if (mParent != null) {
    
    
                mParent.requestChildFocus(this, this);
                updateFocusedInCluster(oldFocus, direction);
            }

            if (mAttachInfo != null) {
    
    
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }
            onFocusChanged(true, direction, previouslyFocusedRect);
            refreshDrawableState();
        }
    }

Where did the first ACTION_DOWN go?
Since EarlyPostImeInputStage.onProcess returns FINISH_HANDLED , subsequent distributions are also stopped.
In the normal flow, the dispatch of keys goes further:

//ACTION_DOWN的分发
ViewRootImpl  D   onDeliverToNext ViewPreImeInputStage
ViewRootImpl  D   onDeliverToNext ImeInputStage
ViewRootImpl  D   onDeliverToNext EarlyPostImeInputStage
KeybuttonTest D   > onFocusChange true

//ACTION_UP
ViewRootImpl  D   onDeliverToNext NativePostImeInputStage
ViewRootImpl  D   onDeliverToNext ViewPostImeInputStage
ViewRootImpl  D   onDeliverToNext SyntheticInputStage
ViewRootImpl  D   onDeliverToNext ViewPreImeInputStage
ViewRootImpl  D   onDeliverToNext ImeInputStage
ViewRootImpl  D   onDeliverToNext EarlyPostImeInputStage
ViewRootImpl  D   onDeliverToNext NativePostImeInputStage
ViewRootImpl  D   onDeliverToNext ViewPostImeInputStage

Subsequent transfers:

ViewPostImeInputStage DecorView PhoneWindow ViewGroup KeyEvent View dispatchKeyEvent superDispatchKeyEvent superDispatchKeyEvent dispatchKeyEvent dispatch onKeyDown ViewPostImeInputStage DecorView PhoneWindow ViewGroup KeyEvent View

Simple validation if commented out

if (checkForLeavingTouchModeAndConsume(event)) {
    
    
   //return FINISH_HANDLED;
}

After the test, the result is as expected and the first ACTION_DOWN can be received normally.


Some harvests
InputDispatcher distributes events to the app process through the creation of InputChannel
insert image description here
InputChannel
insert image description here
insert image description here

reference

Android InputEvent framework implementation and transfer process (app side)
Android source code analysis-input-Java layer
Android Input 3
Android InputMethodService input method processing Input event process combing
from shallow to deep learning android input system (3) - InputChannel analysis
Android Input (5)- InputChannel communication
Input system—UI thread
Input system—the whole process of event processing

Guess you like

Origin blog.csdn.net/ansondroider/article/details/129448707