Android Activity中捕获KEYCODE_DPAD_CENTER按键

平台

    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
有焦点有焦点不传递

分析

第一次触发焦点

  1. 在接收到按键后,EarlyPostImeInputStage.processKeyEvent
  2. 判断是否退出出没模式: checkForLeavingTouchModeAndConsume
  3. 从isNavigationKey函数中可得知, KEYCODE_DPAD_CENTER也属于导航键
  4. 一路跟下来: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

后续的传递:

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

简单的验证如果注释掉

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系统—事件处理全过程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值