使用 Jetpack Compose 添加对键盘、鼠标、触控板和触控笔的支持

1. 简介

如果您的应用适用于标准手机,就应该也适用于大屏设备,例如平板电脑、可折叠设备和 ChromeOS 设备。

用户期望您的应用在大屏设备上能够提供足以媲美小屏设备(甚至更出色)的用户体验。

此外,在大屏设备上,用户通过实体键盘和指控设备(例如鼠标或触控板)使用您应用的可能性更大。一些大屏设备(例如 Chromebook)配有实体键盘和指控设备,也有一些大屏设备可以连接 USB 或蓝牙键盘和指控设备。用户希望在通过实体键盘和指控设备使用您的应用时,能够完成与使用触摸屏时相同的任务。

前提条件

  • 拥有使用 Compose 构建应用的经验
  • 具备 Kotlin 方面的基础知识,包括 lambda 和协程

构建内容

您将为基于 Jetpack Compose 的应用添加对实体键盘和鼠标的支持,具体包括以下步骤:

  1. 按照大屏应用质量指南中定义的标准检查应用
  2. 查看审核结果,确定与实体键盘和鼠标支持相关的问题
  3. 解决问题

更具体地说,您将更新示例应用的以下项目:

  • 键盘导航
  • 用于向下和向上滚动的键盘快捷键
  • 键盘快捷键助手

学习内容

  • 如何审核您的应用是否支持虚拟设备
  • 如何使用 Compose 管理键盘导航
  • 如何使用 Compose 添加键盘快捷键

所需条件

  • Android Studio Hedgehog 或更高版本
  • 以下任意设备(用于运行示例应用):
  • 配备实体键盘和鼠标的大屏设备
  • 具有“桌面设备”定义类别的配置文件的 Android 虚拟设备

2. 设置

  1. 克隆 large-screen-codelabs GitHub 代码库:
git clone https://github.com/android/large-screen-codelabs

或者,您也可以下载并解压 large-screen-codelabs zip 文件:

  1. 导航到 add-keyboard-and-mouse-support-with-compose 文件夹。
  2. 在 Android Studio 中,打开相应项目。add-keyboard-and-cursor-support-with-compose 文件夹中包含一个项目。
  3. 如果您没有 Android 平板电脑或可折叠设备,也没有配备实体键盘和鼠标的 ChromeOS 设备,请在 Android Studio 中打开 Device Manager(设备管理器),然后在 Desktop(桌面设备)类别下创建任意虚拟设备。

“Desktop”(桌面设备)类别下的虚拟设备

3. 探索应用

示例应用会显示一个文章列表。用户可以在该列表中选择文章进行阅读。

应用会根据其窗口宽度以自适应方式更新布局。应用窗口按宽度可分为三个窗口类:紧凑型、中等和扩展型。

按窗口宽度划分的窗口大小类:紧凑型、中等和扩展型。窗口宽度小于 600 dp 的应用窗口归类为紧凑型窗口。如果窗口宽度大于或等于 640 dp,则归类为扩展型窗口。介于紧凑型或扩展型窗口之间的窗口归类为中等窗口。

适用于紧凑型和中等窗口大小类的布局

在这种情况下,应用会使用单窗格布局。在主屏幕上,应用会显示文章列表。当用户选择列表中的某篇文章时,应用将跳转到新页面,显示相应的文章内容。

全局导航是通过抽屉式导航栏实现的。

应用在桌面模拟器中以紧凑型窗口运行。屏幕上显示文章列表。

适用于扩展型窗口大小类的布局

在这种情况下,应用会使用列表-详情布局。列表窗格显示文章列表,而详情窗格则显示所选文章内容。

全局导航是通过侧边导航栏实现的。

应用在桌面模拟器中以扩展型窗口大小类运行。

4. 背景

Compose 提供了多种 API,可帮助应用处理来自实体键盘和鼠标的事件。某些 API 支持键盘和鼠标事件的处理,其方式与处理触摸事件类似。因此,在许多用例中,您的应用会自动支持实体键盘和鼠标,您无需执行任何开发工作。

一个典型示例是 clickable 修饰符,它支持点击检测。手指点按会被检测为点击,而鼠标点击和按 Enter 键也会被检测为点击。如果您的应用使用 clickable 修饰符检测点击,那么无论用户使用何种输入设备,都可以与组件互动。

但是,尽管有这项通用 API 支持,为了实现良好的实体键盘和鼠标支持,仍需进行一些开发工作。其中一个原因是,您需要通过测试您的应用来找出极端情况。此外,您还需要做些工作来减少因设备特性给用户带来的不便,例如:

  • 用户不了解他们可以点击哪些组件
  • 用户无法按预期移动键盘焦点
  • 用户使用实体键盘时无法向上或向下滚动页面

键盘焦点

键盘焦点是实体键盘与屏幕轻触两种操作方式在互动方面的主要区别。进行轻触操作时,用户可以不必考虑之前触摸过的组件的位置,随意点按屏幕上的任何组件。相比之下,使用键盘时,用户需要在实际互动开始之前,手动选择要与之互动的组件。选定的对象称为“键盘焦点”。

用户可以使用 Tab 键和方向(或箭头)键移动键盘焦点。键盘焦点默认仅移动到相邻的组件

使用实体键盘时遇到的问题大多与键盘焦点有关。下面列出了一些常见问题:

  • 用户无法将键盘焦点移动到他们想要与之互动的组件处
  • 用户按 Enter 键时,组件检测不到点击
  • 键盘焦点的移动方式与用户预期的不同
  • 页面跳转后,用户需要按多个键才能将键盘焦点移动到他们想要与之互动的组件处
  • 由于没有直观的提示来指明键盘焦点,导致用户无法确定键盘焦点所在的组件
  • 用户在导航到新页面时无法确定键盘焦点所在的默认组件

以直观的方式指明键盘焦点的所在位置非常重要。否则,用户可能会在应用中感到不知所措,而且不清楚按 Enter 键会发生什么。突出显示是指明键盘焦点的一种典型且直观的方式。用户可以看到键盘焦点位于右侧卡片上的按钮处,因为该按钮处于突出显示状态。

53ee7662b764f2dd.png

键盘快捷键

用户希望在通过实体键盘使用您的应用时,能够使用常用的键盘快捷键。某些组件默认支持标准键盘快捷键。BasicTextField 就是一个典型示例。它支持用户使用标准的文本编辑键盘快捷键,包括:

快捷键

功能

Ctrl+C

复制

Ctrl+X

剪切

Ctrl+V

粘贴

Ctrl+Z

撤消

Ctrl+Y

重做

您的应用可以通过处理按键事件来添加键盘快捷键。借助 onKeyEvent 修饰符和 onPreviewKeyEvent 修饰符,您可以监控按键事件。

指控设备:鼠标、触控板和触控笔

您的应用能够以相同的方式处理鼠标、触控板和触控笔事件。在触控板上的点按操作会被 clickable 修饰符检测为点击。使用触控笔点按也会被检测为点击。

对用户而言,能够直观地了解某个组件能否点击非常重要。因此,“大屏应用质量指南”中提到了悬停状态

Material 3 组件默认支持悬停状态。Material 3 还能提供悬停状态的视觉效果。您可以使用 indication 修饰符将其应用于交互式组件。

滚动

可滚动容器默认支持鼠标滚轮滚动、触控板上的滚动手势,以及使用 Page upPage down 键进行的滚动操作。

对于水平滚动,如果您的应用可以在悬停状态下显示向左和向右的箭头按钮,则会极大地方便用户,让他们能够通过点击这些按钮来滚动浏览内容。

17feb4d3bf08831e.png

设备连接和断开连接时的配置更改

用户应该能够在应用运行时连接或断开键盘和鼠标。当用户遇到需要输入大量文字的文本字段时,可能会连接实体键盘进行输入。蓝牙鼠标会在进入睡眠模式时断开蓝牙连接。通过 USB 连接的键盘可能会意外断开连接。

外围设备硬件的连接或断开会触发配置更改。在整个配置更改期间,您的应用都应保持相同的状态。如需了解详情,请参阅保存界面状态

5. 使用键盘和鼠标检查示例应用

要开始对实体键盘和鼠标支持进行相关开发工作,请启动示例应用并确认以下内容:

  • 用户应该能够将键盘焦点移动到所有交互式组件处
  • 用户应该能够使用 Enter 键“点击”键盘焦点所在的组件
  • 交互式组件获得键盘焦点时,应以某种方式加以指明。
  • 键盘焦点可以通过 Tab 键、Shift+Tab 键和方向(箭头)键按用户预期的方式移动(即遵循既定惯例)
  • 交互式组件应具有悬停状态
  • 用户应该能够点击交互式组件
  • 右键点击或者按住 Control 键点击相应组件(例如长按或选中文字后可显示上下文菜单的组件)时,会显示上下文菜单

在此 Codelab 中,您应该对所有项目进行两次操作:一次是在单窗格布局中进行,另一次是在列表-详情布局中进行。

此 Codelab 中需要解决的问题

您应该遇到了一些问题。在此 Codelab 中,您将解决以下问题:

  • 用户无法仅使用实体键盘阅读整篇文章,因为他们无法向下滚动文章
  • 用户无法确定详情窗格中是否获得了键盘焦点

6. 支持用户在详情窗格中阅读整篇文章

详情窗格中会显示选定的文章。有些文章篇幅较长,如果不滚动页面就无法完整阅读。但是,用户无法仅使用实体键盘来上下滚动浏览这些篇幅较长的文章。

4627289223e5cfbc.gif

可滚动容器(例如 LazyColumn)可以让用户使用 Page down 键向下滚动页面。出现该问题的根本原因是,用户无法将键盘焦点移动到详情窗格。

组件应该能够获取键盘焦点,以接收键盘事件。通过添加 focusable 修饰符,可使修改后的组件获得键盘焦点。

要解决此问题,请按以下步骤操作:

  1. 访问 ui/article/PostContent.kt 文件中的 PostContent 可组合函数
  2. 使用 focusable 修饰符修改 LazyColumn 可组合函数
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostContent(
    post: Post,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    state: LazyListState = rememberLazyListState(),
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    focusRequester: FocusRequester = remember { FocusRequester() },
    header: (@Composable () -> Unit)? = null
) {
    LazyColumn(
        contentPadding = contentPadding,
        modifier = modifier
            .padding(horizontal = defaultSpacerSize)
            .focusable(),
        state = state,
    ) {
      // Code to layout the selected article.
    }
}

指明文章已获得键盘焦点

现在,用户可以使用 Page down 键向下滚动文章来阅读全文。但是,由于缺乏直观的指示,用户很难判断 PostContent 组件是否获得了键盘焦点。

您可以在应用中将 Indication 与组件相关联,直观地指明键盘焦点。Indication 会创建一个对象,以便根据互动来直观呈现键盘焦点。例如,Material 3 的默认 Indication 会在某个组件获得键盘焦点时突出显示该组件。

示例应用具有一个名为 BorderIndicationIndication。该 Indication 会在拥有键盘焦点的组件旁边显示一条线(如下面的屏幕截图所示)。相关代码存储在 ui/components/BorderIndication.kt 文件中。

当文章获得键盘焦点时,文章边缘会显示一条浅灰色线。

如需让 PostConent 可组合项在获得键盘焦点时显示 BorderIndication,请按以下步骤操作:

  1. 访问 ui/article/PostContent.kt 文件中的 PostContent 可组合函数
  2. 声明与 remember() 函数的返回值关联的 interactionSource
  3. remember() 函数中调用 MutableInteractionSource() 函数,以使创建的 MutableInteractionSource 对象与 interactionSource 值相关联
  4. 使用 interactionSource 参数将 interactionSource 值传递给 focusable 修饰符
  5. 更改 PostContent 可组合项的修饰符,以便在调用 indication 修饰符后调用 focusable 修饰符
  6. interactionSource 值和 BorderIndication 函数的返回值传递给 Indication 修饰符
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostContent(
    post: Post,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    state: LazyListState = rememberLazyListState(),
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    focusRequester: FocusRequester = remember { FocusRequester() },
    header: (@Composable () -> Unit)? = null
) {
    val interactionSource = remember { MutableInteractionSource() }

    LazyColumn(
        contentPadding = contentPadding,
        modifier = modifier
            .padding(horizontal = defaultSpacerSize)
            .indication(interactionSource, BorderIndication())
            .focusable(interactionSource = interactionSource),
        state = state,
    ) {
      // Code to layout the selected article.
    }
}

添加用于上下滚动的键盘快捷键

对用户来说,使用 Spacebar 进行上下滚动是一项常用功能。您的应用可以通过添加下表中的键盘快捷键来实现这一功能:

快捷键

功能

Spacebar

向下滚动文章

Shift + Spacebar

向上滚动文章

通过添加 onKeyEvent 修饰符,您的应用将能处理修改后的组件上发生的按键事件。该修饰符接受 lambda,该参数用于处理由描述按键事件的 KeyEvent 对象触发的调用。lambda 应该返回一个 Boolean 值,用于指示按键事件是否被处理。

LazyColumnLazyRow 的滚动位置是通过 LazyListState 对象获取的。通过对 LazyListState 对象调用 animateScrollBy() 挂起方法,您的应用将能够触发滚动操作。该方法会将 LazyColumn 向下滚动指定的像素数。使用负浮点值调用挂起函数时,该函数会将 LazyColumn 向上滚动。

如需实现这些键盘快捷键,请按以下步骤操作:

  1. 访问 ui/article/PostContent.kt 文件中的 PostContent 可组合函数
  2. 使用 onKeyEvent 修饰符修改 LazyColumn 可组合函数
  3. 向传递给 onKeyEvent 修饰符的 lambda 添加 if 表达式,如下所示:
  • 如果满足以下条件,则返回 true
  • Spacebar 键被按下。您可以通过测试 type 属性是否为 KeyType.KeyDown 以及 key 属性是否为 Key.Spacebar 进行检测
  • isCtrlPressed 属性为 false,表示 Ctrl 键未被按下
  • isAltPressed 属性为 false,表示 Alt 键未被按下
  • isMetaPressed 属性为 false,表示 Meta 键未被按下(参见“注意”中的内容)
  • 如果这些条件均不满足,则返回 false
  1. 确定使用 Spacebar 滚动的量,如下所示:
  • 当给定 KeyEvent 对象的 isShiftPressed 属性为 true 时(即用户按下 Shift 键时),返回 -0.4f
  • 否则返回 0.4f
  1. coroutineScope 调用 launch() 方法,前者是 PostContent 可组合函数的参数
  2. 将上一步计算出的相对滚动量与 launch 方法的 lambda 参数中的 state.layoutInfo.viewportSize.height 属性相乘,计算出实际的滚动量。该属性表示在 PostContent 可组合函数中调用的 LazyColumn 的高度。
  3. launch() 方法的 lambda 中调用 state.animateScrollBy() 方法,以触发垂直滚动
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostContent(
    post: Post,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    state: LazyListState = rememberLazyListState(),
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    focusRequester: FocusRequester = remember { FocusRequester() },
    header: (@Composable () -> Unit)? = null
) {
    val interactionSource = remember { MutableInteractionSource() }

    LazyColumn(
        contentPadding = contentPadding,
        modifier = modifier
            .padding(horizontal = defaultSpacerSize)
            .onKeyEvent {
                if (
                    it.type == KeyEventType.KeyDown &&
                    it.key == Key.Spacebar &&
                    !it.isCtrlPressed &&
                    !it.isAltPressed &&
                    !it.isMetaPressed
                ) {

                    val relativeAmount = if (it.isShiftPressed) {
                        -0.4f
                    } else {
                        0.4f
                    }
                    coroutineScope.launch {
                        state.animateScrollBy(relativeAmount * state.layoutInfo.viewportSize.height)
                    }
                    true
                } else {
                    false
                }
            }
            .indication(interactionSource, BorderIndication())
            .focusable(interactionSource = interactionSource),
        state = state,
    ) {
      // Code to layout the selected article.
    }
}

帮助用户了解键盘快捷键

除非用户了解快捷键,否则无法充分利用外接的键盘。您的应用可以通过键盘快捷键助手(Android 系统界面的一部分)向用户显示可用的快捷键。用户可以使用 Meta+/ 打开快捷键助手。

键盘快捷键助手会显示前一部分中添加的键盘快捷键。

应用会替换其主 activity 中的 onProvideKeyboardShortcuts() 方法,以向键盘快捷键助手提供键盘快捷键列表。

更具体地说,您的应用会通过将多个 KeyboardShortcutGroup 对象添加到传递给 onProvideKeyboardShortcuts() 的可变列表,来提供这些对象。每个 KeyboardShortcutGroup 都代表一个已命名的键盘快捷键类别,可以让您的应用按用途或上下文对可用的键盘快捷键进行分组。

示例应用有两个键盘快捷键:SpacebarShift+Spacebar

要使这两个快捷键出现在键盘快捷键助手中,请按照以下步骤操作:

  1. 打开 MainActivity.kt 文件
  2. 替换 MainActivity 中的 onProvideKeyboardShortcuts() 方法
  3. 确保 Android SDK 版本为 Android 7.0(API 级别 24)或更高版本,以便键盘快捷键助手可以正常使用。
  4. 确认方法的第一个参数不是 null
  5. 使用以下参数为 Spacebar 键创建 KeyboardShortcutInfo 对象
  • 说明文字
  • android.view.KeyEvent.KEYCODE_SPACE
  • 0(表示无修饰符)
  1. 使用以下参数为 Shift+Spacebar 创建另一个 KeyboardShortcutInfo
  • 说明文字
  • android.view.KeyEvent.KEYCODE_SPACE
  • android.view.KeyEvent.META_SHIFT_ON
  1. 创建一个包含两个 KeyboardShortcutInfo 对象的不可变列表
  2. 创建一个包含以下参数的 KeyboardShortcutGroup 对象:
  • 文本中的群组名称
  • 上一步中的不可变列表
  1. KeyboardShortcutGroup 对象添加到作为 onProvideKeyboardShortcuts() 方法的第一个参数传递的可变列表中

替换方法如下所示:

   override fun onProvideKeyboardShortcuts(
        data: MutableList<KeyboardShortcutGroup>?,
        menu: Menu?,
        deviceId: Int
    ) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && data != null) {
            val shortcutGroup = KeyboardShortcutGroup(
                "To read articles",
                listOf(
                    KeyboardShortcutInfo("Scroll down", KeyEvent.KEYCODE_SPACE, 0), // 0 means no modifier key is pressed
                    KeyboardShortcutInfo("Scroll up", KeyEvent.KEYCODE_SPACE, KeyEvent.META_SHIFT_ON),
                )
            )
            data.add(shortcutGroup)
        }
    }

开始运行

现在,用户可以使用 Spacebar 滚动文章页面,来阅读整篇文章。您可以尝试使用 Tab 键或方向键在文章中移动键盘焦点。您可以看到鼓励您按 Spacebar 键的消息。

键盘快捷键助手会显示您添加的两个键盘快捷键(按 Meta+/)。添加的快捷键会在 Current app(当前应用)标签页中列出。

7. 在详情窗格中使用键盘更快地进行导航

当应用在扩展型窗口大小类中运行时,用户需要多次按 Tab 键才能将键盘焦点移动到详情窗格。通过右方向键,用户只需一次操作即可将键盘焦点从文章列表移至相应文章,从而实现快速导航。初始焦点位置无法支持用户的主要目标:阅读文章。

您的应用可以使用 FocusRequester 对象来请求将键盘焦点移至特定组件。focusRequester 修饰符会将 FocusRequester 对象与修改后的组件相关联。您的应用可以通过调用 FocusRequester 对象的 requestFocus() 方法,来发送实际的焦点移动请求。

发送键盘焦点移动请求是该组件的附带效果。您的应用应使用 LaunchedEffect 函数以适当的方式调用该方法。

如需将 PostContent 可组合项设置为在用户从文章列表中选择文章时获得键盘焦点,请按以下步骤操作:

  1. 访问 ui/article/ PostContent.kt 文件中的 PostContent 可组合函数。
  2. 使用 focusRequester 修饰符将 focusRequester 值与 LazyColumn 可组合函数相关联。将 focusRequester 值指定为 PostContent 可组合函数的可选参数。
  3. PostContent 可组合函数中,使用它的第一个参数 post 调用 LaunchedEffect,以便在用户选择文章时调用传递的 lambda。
  4. 在传递给 LaunchedEffect 函数的 lambda 中,调用 focusRequester.requestFocus() 方法。

更新后的 PostContent 可组合项如下所示:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostContent(
    post: Post,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
    state: LazyListState = rememberLazyListState(),
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    focusRequester: FocusRequester = remember { FocusRequester() },
    header: (@Composable () -> Unit)? = null
) {
    val interactionSource = remember { MutableInteractionSource() }

    LaunchedEffect(post) {
        focusRequester.requestFocus()
    }

    LazyColumn(
        contentPadding = contentPadding,
        modifier = modifier
            .padding(horizontal = defaultSpacerSize)
            .onKeyEvent {
                if (it.type == KeyEventType.KeyDown && it.key == Key.Spacebar) {
                    val relativeAmount = if (it.isShiftPressed) {
                        -0.4f
                    } else {
                        0.4f
                    }
                    coroutineScope.launch {
                        state.animateScrollBy(relativeAmount * state.layoutInfo.viewportSize.height)
                    }
                    true
                } else {
                    false
                }
            }
            .focusRequester(focusRequester),
            .indication(interactionSource, BorderIndication())
            .focusable(interactionSource = interactionSource),
        state = state,
    ) {
      // Code to layout the selected article.
    }
}

开始运行

现在,当用户在文章列表中选择某篇文章后,键盘焦点会移动到该文章。您还会注意到,当您选择某篇文章后,系统会显示一条消息,建议您使用 Spacebar 键来向下滚动阅读文章内容。

8. 恭喜

祝贺您!您为示例应用添加了对实体键盘和鼠标的支持。这样一来,用户就能仅使用实体键盘或鼠标来选择文章列表中的文章,并直接开始阅读了。

您已学习了以下与添加实体键盘和鼠标支持相关的必备知识:

  • 如何检查应用是否支持实体键盘和鼠标,包括使用模拟器进行检查
  • 如何使用 Compose 管理键盘导航
  • 如何使用 Compose 添加键盘快捷键

您还通过少量代码修改丰富了实体键盘和鼠标支持功能。

现在,您可以使用 Compose 向您的正式版应用添加实体键盘和鼠标支持了。

通过学习更多相关知识,您还能为以下功能添加键盘快捷键:

  • 将所选文章标记为“赞”。
  • 为所选文章添加书签。
  • 与其他应用分享所选文章。

了解详情