2. 电视端焦点导航与 D-pad 按键响应处理

电视端焦点导航与 D-pad 按键响应处理 学习卡片

电视端焦点导航的核心机制

在 Android TV 上,遥控器 D-pad 的方向键不会像手机触摸那样直接命中控件,而是通过 焦点系统在可聚焦视图之间移动。每个可聚焦控件(focusable="true")在获得焦点时触发 onFocusChange,按下确认键(KEYCODE_DPAD_CENTER / KEYCODE_ENTER)时触发 onClick。因此电视交互的关键是:焦点落在哪里、如何移动、按下后响应什么。

遥控器 D-pad ▲ ▼ ◀ ▶ OK KEYCODE_DPAD_* KEYCODE_DPAD_CENTER dispatchKeyEvent Android 焦点系统 FocusFinder findNextFocus() requestFocus() onFocusChange() performClick() onClick() 焦点移动/点击 电视界面 按钮 A 按钮 B 按钮 C 按钮 D 当前焦点 ▶ ▼ 焦点高亮 = 黄色边框 确认键 → onClick 响应 方向键 → 焦点移动

实现步骤

下面用一个 LinearLayout 嵌套两行按钮来演示焦点移动与按键响应。每个按钮设置 focusable="true",并通过 StateListDrawable 或 foreground 实现焦点高亮。

1. 布局文件 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:background="#1a1a2e">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn_a"
            android:layout_width="160dp"
            android:layout_height="80dp"
            android:text="按钮 A"
            android:focusable="true"
            android:background="@drawable/focus_bg"
            android:layout_margin="12dp"/>

        <Button
            android:id="@+id/btn_b"
            android:layout_width="160dp"
            android:layout_height="80dp"
            android:text="按钮 B"
            android:focusable="true"
            android:background="@drawable/focus_bg"
            android:layout_margin="12dp"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn_c"
            android:layout_width="160dp"
            android:layout_height="80dp"
            android:text="按钮 C"
            android:focusable="true"
            android:background="@drawable/focus_bg"
            android:layout_margin="12dp"/>

        <Button
            android:id="@+id/btn_d"
            android:layout_width="160dp"
            android:layout_height="80dp"
            android:text="按钮 D"
            android:focusable="true"
            android:background="@drawable/focus_bg"
            android:layout_margin="12dp"/>
    </LinearLayout>
</LinearLayout>

2. 焦点高亮背景 focus_bg.xml(drawable 目录)

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_focused="true">
        <shape>
            <solid android:color="#3a3a6e"/>
            <stroke android:width="3dp" android:color="#f5c86e"/>
            <corners android:radius="10dp"/>
        </shape>
    </item>
    <item>
        <shape>
            <solid android:color="#2a2a4e"/>
            <stroke android:width="1dp" android:color="#4a6a8a"/>
            <corners android:radius="10dp"/>
        </shape>
    </item>
</selector>

3. MainActivity.java 按键响应与焦点日志

public class MainActivity extends Activity {
    private static final String TAG = "TVFocus";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        int[] ids = {R.id.btn_a, R.id.btn_b, R.id.btn_c, R.id.btn_d};
        for (int id : ids) {
            Button btn = findViewById(id);
            btn.setOnClickListener(v ->
                Log.d(TAG, "点击: " + ((Button) v).getText()));
            btn.setOnFocusChangeListener((v, hasFocus) ->
                Log.d(TAG, ((Button) v).getText()
                    + (hasFocus ? " 获得焦点" : " 失去焦点")));
        }

        // 启动后默认让按钮 A 获得焦点
        findViewById(R.id.btn_a).requestFocus();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_UP:
            case KeyEvent.KEYCODE_DPAD_DOWN:
            case KeyEvent.KEYCODE_DPAD_LEFT:
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                Log.d(TAG, "方向键: " + KeyEvent.keyCodeToString(keyCode));
                break;
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                Log.d(TAG, "确认键按下");
                break;
        }
        return super.onKeyDown(keyCode, event);
    }
}
电视模拟器中,方向键对应键盘方向键,确认键对应回车键。运行后观察 Logcat 中 TVFocus 标签的输出,确认焦点在 A→B→D→C 之间循环移动,且每次按下确认键都有对应点击日志。

验证要点

操作预期现象日志验证
启动应用按钮 A 显示黄色焦点边框按钮 A 获得焦点
按 → 键焦点从 A 移到 BA 失去焦点,B 获得焦点
按 ↓ 键焦点从 B 移到 DB 失去焦点,D 获得焦点
按 ← 键焦点从 D 移到 CD 失去焦点,C 获得焦点
按确认键当前焦点按钮触发点击输出“点击: 按钮 X”
如果焦点没有按预期移动,检查按钮是否设置了 android:focusable="true",以及父布局是否意外拦截了焦点。电视上默认焦点顺序由布局层级和 nextFocusUp/Down/Left/Right 属性共同决定,必要时可显式指定。