使用 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 中打开 Device Manager,然后在 Desktop 类别中创建任意虚拟设备。

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 连接的键盘可能会意外断开。

外围硬件的连接或断开会触发配置更改。您的应用应在配置更改期间保留其状态。有关详细信息,请参阅保存界面状态

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(空格键)向上和向下滚动是一项常见功能。您的应用可以通过添加如下表所示的键盘快捷键来实现该功能:

快捷键

功能

空格键

向下滚动文章

Shift + 空格键

向上滚动文章

使用 onKeyEvent 修饰符,您的应用可以处理在修改后的组件上发生的按键事件。该修饰符接受一个 lambda 表达式,该表达式在调用时会传入一个描述按键事件的 KeyEvent 对象。该 lambda 应返回一个 Boolean 值,指示该按键事件是否已被消费。

通过在 LazyListState 对象上调用 animateScrollBy() suspend 方法,您的应用可以触发滚动。LazyColumnLazyRow 的滚动位置被捕获在一个 LazyListState 对象中。该方法根据指定的像素数向下滚动 LazyColumn。当 suspend 函数使用负浮点值调用时,该函数会向上滚动 LazyColumn

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

  1. 访问 ui/article/PostContent.kt 文件中的 PostContent 可组合函数
  2. 使用 onKeyEvent 修饰符修改 LazyColumn 可组合函数
  3. 将一个 if 表达式添加到传递给 onKeyEvent 修饰符的 lambda 中,如下所示:
  • 如果满足以下条件,则返回 true
  • 按下 Spacebar(空格键)。您可以通过测试 type 属性是否为 KeyType.KeyDown 以及 key 属性是否为 Key.Spacebar 来检测。
  • 确保 isCtrlPressed 属性为 false,以确保未按下 Ctrl
  • 确保 isAltPressed 属性为 false,以确保未按下 Alt
  • 确保 isMetaPressed 属性为 false,以确保未按下 Meta 键(参见注意)
  • 否则返回 false
  1. 确定使用 Spacebar 的滚动量如下:
  • 当按下 Shift 键时(由给定的 KeyEvent 对象的 isShiftPressed 属性描述),为 -0.4f
  • 否则为 0.4f
  1. coroutineScopePostContent 可组合函数的一个参数)上调用 launch() 方法
  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+/ 打开快捷键帮助程序。

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

您的应用会重写应用主 Activity 中的 onProvideKeyboardShortcuts() 方法,以便向键盘快捷键帮助程序提供键盘快捷键列表。

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

示例应用有两个键盘快捷键:Spacebar(空格键)和 Shift+Spacebar(Shift + 空格键)。

要使这两个快捷键在键盘快捷键帮助程序中可用,请按照以下步骤操作:

  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. 使用 post 调用 LaunchedEffectpostPostContent 可组合函数的第一个参数,这样当用户选择一篇文章时,传入的 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 为您的生产应用添加物理键盘和鼠标支持了。

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

  • 将选定的文章标记为已赞。
  • 为选定的文章添加书签。
  • 将选定的文章分享给其他应用。

了解更多