使用 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 中打开设备管理器,然后创建桌面类别中的任何虚拟设备。

Virtual devices in the Desktop category

3. 探索应用

示例应用显示文章列表。用户可以阅读从列表中选择的文章。

应用会根据应用窗口的宽度自适应地更新布局。有三个窗口类用于对应用窗口的宽度进行分类:紧凑型、中型和扩展型。

Window size classes on window width: compact, medium, and expanded. The application window with is smaller than  600 dp, the window width is categorized as compact. The window width is categorized as expanded when its width is greater than or equal to 640 dp. The winow does not belong to compact or expanded, its window size class is medium.

紧凑型和中型窗口尺寸类的布局

应用使用单窗格布局。在主屏幕上,应用显示文章列表。当用户从列表中选择一篇文章时,会发生屏幕过渡,并显示该文章。

全局导航是使用导航抽屉实现的。

The  app is running on a Desktop emulator in a compact window. The article list is displayed.

扩展型窗口尺寸类的布局

应用使用列表-详情布局。列表窗格显示文章列表。详情窗格显示所选文章。

全局导航是使用导航轨道实现的。

The app is running on a Desktop emulator in expanded window size class.

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 修饰符允许您监视键事件。

指向设备:Mouse、触控板和触控笔

您的应用可以以相同的方式处理鼠标、触控板和触控笔。clickable 修饰符会将触控板上的轻触检测为点击。使用触控笔轻触也会被检测为点击。

用户能够直观地了解他们是否可以点击某个组件非常重要。这就是为什么悬停状态在大型屏幕应用质量指南中被提及的原因。

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

滚动

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

对于水平滚动,如果您的应用在悬停状态下显示左右箭头按钮,以便用户可以通过点击按钮滚动内容,则会非常用户友好。

17feb4d3bf08831e.png

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

用户可以在您的应用运行时连接或断开键盘和鼠标。用户可能在看到文本字段需要输入大量文本时连接物理键盘。蓝牙连接的鼠标在进入睡眠模式时会断开连接。USB 连接的键盘可能会意外断开。

外围硬件的连接或断开会触发配置更改。您的应用应在整个配置更改过程中保持其状态。有关更多信息,请参阅保存 UI 状态

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

要开始开发物理键盘和鼠标支持的工作,请启动示例应用并确认以下内容

  • 用户应该能够将键盘焦点移动到所有交互式组件
  • 用户应该能够使用Enter键“点击”聚焦的组件
  • 交互式组件在获得键盘焦点时应显示指示
  • 键盘焦点按用户预期移动(即,根据既定约定)使用Tab键、Shift+Tab和方向(箭头)键
  • 交互式组件应具有悬停状态
  • 用户应该能够点击交互式组件
  • 上下文菜单通过在相应的组件上右键单击(辅助控制-单击)出现,例如那些在长按或文本选择时显示上下文菜单的组件

您应该在本代码实验室中遍历所有项目两次:一次用于单窗格布局,一次用于列表-详细信息布局。

在本代码实验室中需要修复的问题

您应该会发现问题。在本代码实验室中,您将修复以下问题

  • 用户无法仅使用物理键盘阅读整篇文章,因为他们无法向下滚动文章
  • 用户无法确定详细信息窗格是否具有键盘焦点

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。该指示在具有键盘焦点的组件旁边显示一条线(如以下屏幕截图所示)。代码存储在ui/components/BorderIndication.kt文件中。

A light gray line is displayed on the side of the article when the article has the keyboard focus.

要使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上下滚动。您的应用可以通过添加键盘快捷键来实现此功能,如下表所示

快捷键

功能

空格键

向下滚动文章

Shift + 空格键

向上滚动文章

onKeyEvent修饰符使您的应用能够处理修改后的组件上发生的按键事件。该修饰符采用一个 lambda,该 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滚动的数量,如下所示
  • 当按下Shift键时为-0.4f,由给定KeyEvent对象的isShiftPressed属性描述
  • 否则为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 系统 UI 的一部分)让用户了解可用的快捷键。用户可以使用Meta+/打开快捷键帮助程序。

Keyboard Shortcut Helper shows the keyboard shortcuts added in the previous section.

您的应用在应用的主活动中覆盖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+/)。添加的快捷键列在“当前应用”选项卡中。

7. 加速详细信息窗格中的键盘导航

当应用在扩展窗口大小类别中运行时,用户需要多次按下Tab键才能将键盘焦点移动到详细信息窗格。使用右侧方向键,用户可以一键将键盘焦点从文章列表移动到文章,但他们仍然需要移动键盘焦点。初始焦点不支持用户阅读文章的主要目标。

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

发送移动键盘焦点的请求是组件的副作用。您的应用应使用LaunchedEffect函数以正确的方式调用该方法。

要设置PostContent可组合项以在用户从文章列表中选择文章时获取键盘焦点,请按照以下步骤操作

  1. ui/article/PostContent.kt文件中访问PostContent可组合函数。
  2. 使用focusRequester修饰符将focusRequester值与LazyColumn可组合函数关联。focusRequester值作为PostContent可组合函数的可选参数指定。
  3. 使用postPostContent可组合函数的第一个参数)调用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 将物理键盘和鼠标支持添加到您的生产应用中。

通过进一步学习,您可以为以下功能添加键盘快捷键

  • 将所选文章标记为喜欢。
  • 将所选文章添加书签。
  • 与其他应用共享所选文章。

了解更多