平台
RK3568 + Android 11 + AndroidStuido
概述
测试代码
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>
问题:
正常情况下,Activity可以正捕获到按键的down和up事件, 但是, 当输入的按键是 KEYCODE_DPAD_CENTER、KEYCODE_ENTER后,只接收到了一次ACTION_UP, 且后续onKeyDown、onKeyDown都没有监听到按键进来。
从Android的事件分发机制来看,初步判断是按键被某一个控件捕获了。而布局中,只有一个TextView控件。
一些尝试
- 方案一: 禁止TextView焦点捕获
findViewById(R.id.tvInfo).setFocusable(false);
测试功能正常,Activity可以正常捕获按键事件。
在后续的方案中,第一次的KeyDown,都被用于处理控件焦点
- 方案二: 在分发前捕获
@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)
- 方案三: 重写View控件的onKeyDown/onKeyUp
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
若给控键增加焦点时间监听,可以看出:
当按下KEYCODE_DPAD_CENTER时,是否有焦点处理是不一样的
第一次 | 当前焦点状态 | 按下后状态 | 是否传递给Activity | 是否触发OnClick |
---|---|---|---|---|
是 | 无焦点 | 有焦点 | 只传了onKeyUp | 否 |
否 | 有焦点 | 有焦点 | 不传递 | 是 |
分析
第一次触发焦点
- 在接收到按键后,EarlyPostImeInputStage.processKeyEvent
- 判断是否退出出没模式: checkForLeavingTouchModeAndConsume
- 从isNavigationKey函数中可得知, KEYCODE_DPAD_CENTER也属于导航键
- 一路跟下来: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();
}
最终View获取到焦点:
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();
}
}
第一个ACTION_DOWN哪里去了?
由于EarlyPostImeInputStage.onProcess中返回了FINISH_HANDLED,所以,后续的分发也都停止了。
正常流程中,按键的分发会走得更远:
//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
后续的传递:
简单的验证如果注释掉
if (checkForLeavingTouchModeAndConsume(event)) {
//return FINISH_HANDLED;
}
测试后结果如预期 第一个ACTION_DOWN 可以正常收到。
一些收获
InputDispatcher将事件分发给app进程是通过InputChannel
InputChannel的创建
参考
Android InputEvent框架实现及传递过程(app端)
Android 源码分析 - 输入 - Java层
Android Input 3
Android InputMethodService输入法处理Input事件过程梳理
由浅入深学习android input系统(三) - InputChannel解析
Android Input(五)-InputChannel通信
Input系统—UI线程
Input系统—事件处理全过程