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

1. 简介

当您的应用可用于标准手机时,它也适用于大屏幕设备(例如平板电脑、折叠屏设备和 ChromeOS 设备)。

用户期望您的应用在大屏幕上的用户体验与小屏幕上的 UX 相当或更好。

用户也更有可能在大屏幕设备上使用您的应用与物理键盘和指向设备(例如鼠标或触控板)配合使用。一些大屏幕设备(例如 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 修饰符允许您监控按键事件。

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

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

对于用户来说,能够直观地理解他们是否可以点击某个组件非常重要。这就是为什么 悬停状态 在大屏幕应用程序质量指南中被提及。

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

滚动

可滚动容器默认情况下支持鼠标滚轮滚动、触控板上的滚动手势以及使用 Page UpPage Down 键滚动。

对于水平滚动,如果您的应用程序在悬停状态下显示左右箭头按钮,以便用户可以通过点击按钮滚动内容,这将非常人性化。

17feb4d3bf08831e.png

通过设备连接和断开连接进行的配置更改

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

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

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

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

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

您应该在此 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。该指示在具有键盘焦点的组件旁边显示一条线(如以下屏幕截图所示)。代码存储在 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 向上和向下滚动。您的应用程序可以通过添加键盘快捷键来实现此功能,如下表所示

快捷键

功能

Spacebar

向下滚动文章

Shift + Spacebar

向上滚动文章

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() 方法,coroutineScopePostContent 可组合函数的参数
  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() 方法,以便向键盘快捷键帮助程序提供键盘快捷键列表。

更具体地说,您的应用程序通过将它们添加到传递给 onProvideKeyboardShortcuts() 的可变列表中来提供多个 KeyboardShortcutGroup 对象。每个 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 将物理键盘和鼠标支持添加到您的生产应用中。

通过更多学习,您可以为以下功能添加键盘快捷键

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

了解更多