Compose 中的键盘焦点管理

1. 简介

用户可以在平板电脑、ChromeOS 设备等大屏设备以及扩展现实设备上,通过硬件键盘与您的应用进行互动。用户使用硬件键盘在应用中导航时能达到和使用触摸屏时同样的效果,这一点非常重要。此外,您的应用面向电视和车载显示屏设计,由于这些设备可能不支持触控输入,而是依靠方向键或旋转编码器进行操作,因此您需要采用类似的键盘导航原则。

借助 Compose,您可以统一处理来自硬件键盘、方向键和旋转编码器的输入。对于这些输入方式而言,良好用户体验的关键原则在于,用户能够直观且一致地将键盘焦点移至他们希望互动的互动式组件上。

在此 Codelab 中,您将学习以下内容:

  • 如何实现常见的键盘焦点管理模式,从而实现直观且一致的导航体验
  • 如何测试键盘焦点移动是否按预期运行

前提条件

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

构建内容

您实现以下典型的键盘焦点管理模式:

  • 键盘焦点移动 - 键盘焦点以 Z 形模式,从起始位置开始,自上而下、从左到右移动
  • 逻辑初始焦点 - 将焦点设置为用户可能与之互动的界面元素
  • 焦点恢复功能 - 将焦点移至用户之前互动过的界面元素

学习内容

  • Compose 中的焦点管理基础知识
  • 如何将界面元素设为焦点目标
  • 如何请求焦点以移动界面元素
  • 如何将键盘焦点移至一组界面元素中的某个特定界面元素

所需条件

  • Android Studio Ladybug 或更高版本
  • 以下任意设备(用于运行示例应用):
  • 配备硬件键盘的大屏设备
  • 适用于大屏设备的 Android 虚拟设备,例如可调整大小的模拟器

2. 设置

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

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

  1. 导航到 focus-management-in-compose 文件夹。
  2. 在 Android Studio 中,打开相应项目。focus-management-in-compose 文件夹中包含一个项目。
  3. 如果您没有 Android 平板电脑、可折叠设备或配备硬件键盘的 ChromeOS 设备,请在 Android Studio 中打开设备管理器,然后在手机类别中创建可调整大小的设备。

Android Studio 的“设备管理器”会在“手机”类别中显示可用的虚拟设备列表。可调整大小的模拟器属于此类别。 **图 1.**在 Android Studio 中配置可调整大小的模拟器。

3. 查看起始代码

该项目包含两个模块:

  • start - 包含该项目的起始代码。您将更改该代码才能完成此 Codelab。
  • solution — 包含此 Codelab 中已完成的代码。

示例应用包含以下三个标签页:

  • 焦点目标
  • 焦点遍历顺序
  • 焦点小组

应用启动时,系统会显示焦点目标标签页。

示例应用的第一个视图。它包含三个标签页,并且焦点目标标签页(即第一个标签页)处于选中状态。该标签页显示三张卡片,并且它们位于一列中。

图 2. 应用启动时,系统会显示 Focus target(焦点目标)标签页。

ui 软件包中包含您将互动的以下界面代码:

4. 焦点目标

焦点目标是键盘焦点可以移至的界面元素。用户可以按 Tab 键或方向(箭头)键移动键盘焦点:

  • Tab 键 - 焦点沿一维方向移动到下一个或上一个焦点目标。
  • 方向键 - 焦点可以在二维空间内向上、向下、向左和向右移动。

标签页属于焦点目标。在示例应用中,当标签页获得焦点时,其背景会发生视觉上的变化。

GIF 动画文件显示,键盘焦点在界面元素中移动的方式。它会在三个标签页之间移动,然后聚焦“第 1 张卡片”。

图 3. 当焦点移至焦点目标时,组件背景会发生变化。

默认情况下,互动式界面元素是焦点目标

默认情况下,互动式组件是焦点目标。换言之,如果用户可以点按某个界面元素,则该界面元素就是焦点目标。

在示例应用的焦点目标标签页中,有三张卡片。1st card(第 1 张卡片)和3rd card(第 3 张卡片)是焦点目标;2nd card(第 2 张卡片)则不是。当用户按 Tab 键将焦点从第 1 张卡片移至第 3 张卡片时,第 3 张卡片的背景会更新。

GIF 动画显示,焦点目标标签页中的初始键盘焦点移动。当用户在“第 1 张卡片”上按 Tab 键时,焦点会跳过“第 2 张卡片”,并从“第 1 张卡片”移至“第 3 张卡片”。

图 4. 应用焦点目标不包括第 2 张卡片。

将“第 2 张卡片”设置为焦点目标

您可以将第 2 张卡片更改为互动式界面元素,从而使其成为焦点目标。最简单的方法是使用如下所示的 clickable 修饰符:

  1. 打开 tabs 软件包中的 FocusTargetTab.kt
  2. 使用 clickable 修饰符修改 SecondCard 可组合项,如下所示:
@Composable
fun FocusTargetTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(240.dp)
        )
        SecondCard(
            modifier = Modifier
                .width(240.dp)
                .clickable(onClick = onClick)
        )
        ThirdCard(
            onClick = onClick,
            modifier = Modifier.width(240.dp)
        )
    }
}

开始运行

现在,用户不仅能够将焦点移动到第 1 张卡片和第 3 张卡片,还可以将焦点移至第 2 张卡片。您可以在焦点目标标签页中进行尝试,确认您可以使用 Tab 键将焦点从第 1 张卡片顺利移至第 2 张卡片。

GIF 动画显示,修改后的键盘焦点移动情况。当用户在“第 1 张卡片”上按 Tab 键时,焦点会从“第 1 张卡片”移开。

图 5. 按 Tab 键将焦点从第 1 张卡片移至第 2 张卡片。

5. 以 Z 形模式进行焦点遍历

在采用从左到右的语言设置中,用户希望键盘焦点按从左到右、从上到下的顺序移动。这种焦点遍历顺序称为“Z 形模式”。

不过,在 Compose 中,确定 Tab 键的下一个焦点目标时会忽略布局,而是根据可组合函数调用的顺序使用一维焦点遍历方式。

一维焦点遍历

一维焦点遍历顺序由可组合函数调用的顺序决定,而非应用布局。

在示例应用中,焦点在 Focus traversal order(焦点遍历顺序)标签页中按以下顺序移动:

  1. 第 1 张卡片
  2. 第 4 张卡片
  3. 第 3 张卡片
  4. 第 2 张卡片

GIF 动画显示,键盘焦点的移动方式与用户的预期不同。焦点从“第 1 张卡片”移至“第 3 张卡片”,然后移至“4th card”(第 4 张卡片)和“第 2 张卡片”。这可能会与用户的预期不同。

图 6. 焦点遍历遵循可组合函数的调用顺序。

FocusTraversalOrderTab 函数实现了示例应用的 Focus traversal(焦点遍历)标签页。该函数会按以下顺序依次调用卡片的可组合函数:FirstCard、FourthCard、ThirdCard 和 SecondCard。

@Composable
fun FocusTraversalOrderTab(
    modifier: Modifier = Modifier
) {
    Row(
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        Column(
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            FirstCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier
                    .width(240.dp)
                    .offset(x = 256.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier
                    .width(240.dp)
                    .offset(y = (-151).dp)
            )
        }
        SecondCard(
            modifier = Modifier.width(240.dp)
        )
    }
}

Z 形模式下的焦点移动

您可以按照以下步骤在示例应用的焦点遍历顺序标签页中集成 Z 形焦点移动方式:

  1. 打开“tabs.FocusTraversalOrderTab.kt”
  2. 从 ThirdCard 和 FourthCard 可组合项中移除偏移修饰符。
  3. 将标签页的布局从当前的两列一行更改为一列两行。
  4. 将 FirstCard 和 SecondCard 可组合项移至第一行。
  5. 将 ThirdCard 和 FourthCard 可组合项移至第二行。

修改后的代码如下所示:

@Composable
fun FocusTraversalOrderTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            FirstCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp),
            )
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
        }
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
        }
    }
}

开始运行

现在,用户可以按 Z 形模式,将焦点从右向左、从上到下移动。您可以在焦点遍历顺序标签页中进行尝试,确认是否可以使用 Tab 键,按以下顺序移动焦点:

  1. 第 1 张卡片
  2. 第 2 张卡片
  3. 第 3 张卡片
  4. 第 4 张卡片

GIF 动画显示,修改后键盘焦点的移动方式。它会按 Z 形顺序从左到右、从上到下移动。

图 7. 以 Z 形模式进行焦点遍历。

6. focusGroup

在 Focus group(焦点小组)标签页上按 right 方向键,将焦点从第 1 张卡片移至第 3 张卡片。由于这两张卡片并非并排显示,这种焦点移动可能会让用户感到有些困惑。

GIF 动画显示,键盘焦点如何通过向右方向键从“第 1 张卡片”移至“第 3 张卡片”。这两张卡片位于不同的行中。

图 8. 焦点从第 1 张卡片意外移至第 3 张卡片。

二维焦点遍历与布局信息相关

按方向键会触发二维焦点遍历。在电视上,这是常见的焦点遍历方式,用户可以使用方向键与应用互动。同样,按键盘箭头键也会触发二维焦点遍历,因为它们模拟了方向键的导航功能。

在二维焦点遍历中,系统会参考界面元素的几何信息,从而确定下一个焦点目标。例如,在焦点目标标签页中,按 down 方向键,焦点会移至第 1 张卡片,而按向上方向键,焦点会移至焦点目标标签页。

GIF 动画显示,在焦点目标标签页中,按向下方向键,焦点会移至“第 1 张卡片”,然后按向上方向键,则返回到该标签页。在垂直方向上,这两个焦点目标距离最近。

图 9. 使用向下和向上方向键进行焦点遍历。

与使用 Tab 键进行一维焦点遍历不同,二维焦点遍历不会循环。例如,当第 2 张卡片获得焦点时,用户按向下键焦点不会移动。

GIF 动画显示,即使用户按向下方向键,焦点仍在第 2 张卡片上,因为这张卡片下面没有焦点目标了。

图 10. 当第 2 张卡片处于焦点状态时,按向下方向键无法移动焦点。

焦点目标位于同一级别

以下代码实现了上述界面布局。界面中有四种焦点目标:FirstCard、SecondCard、ThirdCard 和 FourthCard。这四个焦点目标处于同一级别,ThirdCard 是布局中 FirstCard 右侧的第一项。因此,按 right 方向键时,焦点将从第 1 张卡片移至第 3 张卡片。

@Composable
fun FocusGroupTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier,
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(208.dp)
        )
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
        ) {
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
        }
    }
}

使用 focusGroup 修饰符对焦点目标进行分组

您可以按照以下步骤,更改这种可能引起困惑的焦点移动方式:

  1. 打开“tabs.FocusGroup.kt”
  2. 使用 focusGroup 修饰符,修改 FocusGroupTab 可组合函数中的 Column 可组合函数。

更新后的代码如下所示:

@Composable
fun FocusGroupTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier,
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(208.dp)
        )
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
            modifier = Modifier.focusGroup(),
        ) {
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
        }
    }
}

focusGroup 修饰符会创建一个焦点小组,其中包含修改后的组件内的焦点目标。焦点小组内的焦点目标和焦点小组外的焦点目标位于不同级别,并且 FirstCard 可组合项的右侧没有其他焦点目标。因此,按 right 方向键时,焦点不会从第 1 张卡片移至任何其他卡片。

开始运行

现在,在示例应用的焦点小组标签页中,按 right 方向键,焦点不会从第 1 张卡片移至第 3 张卡片。

7. 请求焦点

用户无法使用键盘或方向键直接选择任意界面元素进行互动。用户需要先将键盘焦点移至某个互动式组件,然后才能与该元素互动。

例如,用户需要将焦点从焦点目标标签页移至第 1 张卡片,然后才能与卡片互动。通过合理设置初始焦点,可以减少用户启动主要任务所需的操作次数。

GIF 动画显示,用户应在选择标签页后按 Tab 键三次,才能将键盘焦点移至该标签页中的第 1 张卡片。

图 11. 按三次 Tab 键可将焦点移至第 1 张卡片。

使用 FocusRequester 请求焦点

您可以使用 FocusRequester 请求焦点,从而移动界面元素。在调用 requestFocus() 方法之前,应先将 FocusRequester 对象与界面元素相关联。

将初始焦点设置为“第 1 张卡片”

您可以通过以下步骤将初始焦点设置为第 1 张卡片:

  1. 打开“tabs.FocusTarget.kt”
  2. 在 FocusTargetTab 可组合函数中声明 firstCard 值,并使用 remember 函数返回的 FocusRequester 对象初始化该值。
  3. 使用 focusRequester 修饰符修改 FirstCard 可组合函数。
  4. 将 firstCard 值指定为 focusRequester 修饰符的参数。
  5. 使用 Unit 值调用 LaunchedEffect 可组合函数,并在传递给 LaunchedEffect 可组合函数的 lambda 中,对 firstCard 值调用 requestFocus() 方法。

在第二步和第三步中,会创建一个 FocusRequester 对象并将其与界面元素相关联。在第五步中,在首次组合 FocusdTargetTab 可组合项时,系统会请求将焦点移至关联的界面元素。

更新后的代码如下所示:

@Composable
fun FocusTargetTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    val firstCard = remember { FocusRequester() }

    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier
                .width(240.dp)
                .focusRequester(focusRequester = firstCard)
        )
        SecondCard(
            modifier = Modifier
                .width(240.dp)
                .clickable(onClick = onClick)
        )
        ThirdCard(
            onClick = onClick,
            modifier = Modifier.width(240.dp)
        )
    }

    LaunchedEffect(Unit) {
        firstCard.requestFocus()
    }
}

开始运行

现在,选择焦点目标标签页时,键盘焦点会移至该标签页中的第 1 张卡片。您可以通过切换标签页来尝试一下。此外,应用启动时,第 1 张卡片也会处于选中状态。

GIF 动画显示,当用户选择焦点目标标签页时,键盘焦点会自动移至第 1 张卡片。

图 12. 选择焦点目标标签页后,焦点会移至第 1 张卡片。

8. 将焦点移至所选标签页

您可以在键盘焦点进入焦点小组时,指定具体的焦点目标。例如,当用户将焦点移至标签页行时,你可以将焦点移至选中的标签页。

您可以通过以下步骤实现此行为:

  1. 打开 App.kt。
  2. 在 App 可组合函数中声明 focusRequesters 值。
  3. 将 focusRequesters 值初始化为 remember 函数的返回值,该返回值是由 FocusRequester 对象组成的列表。返回列表的长度应与 Screens.entries 的长度相等。
  4. 使用 focusRequester 修饰符修改 Tab 可组合项,将 focusRequester 值的每个 FocusRequester 对象与 Tab 可组合项相关联。
  5. 使用 focusProperties 修饰符和 focusGroup 修饰符修改 PrimaryTabRow 可组合项。
  6. 将 lambda 传递给 focusProperties 修饰符,并将 onEnter 属性与另一个 lambda 相关联。
  7. 在 enter 属性关联的 lambda 表达式中,从 focusRequesters 列表中选取与 selectedTabIndex 对应的元素并调用 requestFocus 方法。

修改后的代码如下所示:

@Composable
fun App(
    modifier: Modifier = Modifier,
) {
    val context = LocalContext.current

    val backstack = rememberNavBackStack(Tab.FocusTarget)
    val selectedTabIndex = Tab.entries.indexOf(backstack.last())
    val focusRequesters = remember {
        List(Tab.entries.size) { FocusRequester() }
    }

    Column(modifier = modifier) {
        PrimaryTabRow(
            selectedTabIndex = selectedTabIndex,
            modifier = Modifier
                .focusProperties {
                    onEnter = {
                        focusRequesters[selectedTabIndex].requestFocus()
                    }
                }
                .focusGroup()
        ) {
            Tab.entries.forEachIndexed { index, tab ->
                val isSelected = selectedTabIndex == index
                Tab(
                    selected = isSelected,
                    onClick = {
                        if (!isSelected) {
                            backstack.add(tab)
                        }
                    },
                    text = { Text(stringResource(tab.title)) },
                    modifier = Modifier.focusRequester(focusRequester = focusRequesters[index])
                )
            }
        }
        NavDisplay(
            backStack = backstack,
            entryProvider = entryProvider {
                entry<Tab.FocusTarget> {
                    FocusTargetTab(
                        onClick = context::onCardClicked,
                        modifier = Modifier.padding(32.dp),
                    )
                }
                entry<Tab.FocusTraversalOrder> {
                    FocusTraversalOrderTab(
                        onClick = context::onCardClicked,
                        modifier = Modifier.padding(32.dp)
                    )
                }
                entry<Tab.FocusGroup> {
                    FocusGroupTab(
                        onClick = context::onCardClicked,
                        modifier = Modifier.padding(32.dp)
                    )
                }
            }
        )
    }
}

您可以使用 focusProperties 修饰符控制焦点移动。在传递给修饰符的 lambda 中,修改 FocusProperties。当用户将焦点置于修改后的界面元素上,并按 Tab 键或方向键时,系统会参考 FocusProperties 来选择焦点目标。

当焦点进入焦点小组时,系统会调用为 onEnter 属性设置的 lambda。您可以在 lambda 中请求焦点,从而将焦点移至界面元素。

开始运行

现在,当用户将焦点移至标签页行时,键盘焦点会移至所选的标签页。您可以按以下步骤进行尝试:

  1. 运行应用
  2. 选择焦点小组标签页
  3. 使用 down 方向键将焦点移至第 1 张卡片。
  4. 使用 up 方向键移动焦点。

图 13. 焦点会移至所选标签页。

9. 焦点恢复

用户希望在任务被中断时能够轻松地继续执行任务。焦点恢复功能支持从中断状态恢复,它会将键盘焦点移至之前选择的界面元素。

焦点恢复的一个典型用例是视频串流应用的主屏幕。屏幕上有多个视频内容列表,例如某个类别下的电影列表,或某部电视剧的分集列表。用户会浏览这些列表,寻找感兴趣的内容。有时,用户会返回到之前浏览过的列表,并继续浏览其中的内容。借助焦点恢复功能,用户无需手动将键盘焦点移至列表中最后查看过的项目,即可直接继续浏览。

focusRestorer 修饰符会将焦点恢复到焦点小组

使用 focusRestorer 修饰符可保存并恢复对焦点小组的焦点状态。当焦点离开焦点小组时,焦点会存储对之前获得焦点的项目的引用。然后,当焦点重新进入焦点小组时,焦点会恢复到之前获得焦点的项目上。

将焦点恢复功能与“焦点小组”标签页集成

示例应用的焦点小组标签页中有一行,其中包含第 2 张卡片、第 3 张卡片和第 4 张卡片。

GIF 动画显示,即便“第 3 张卡片”曾获得过焦点,键盘焦点仍会从“第 1 张卡片”移至“第 2 张卡片”。

图 14. 包含第 2 张卡片、第 3 张卡片和第 4 张卡片的焦点小组。

您可以按照以下步骤在该行中集成焦点恢复功能:

  1. 打开“tab.FocusGroupTab.kt”
  2. 使用 focusRestorer 修饰符,修改 FocusGroupTab 可组合项中的 Row 可组合项。应先调用该修饰符,然后在调用 focusGroup 修饰符。

修改后的代码如下所示:

@Composable
fun FocusGroupTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier,
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(208.dp)
        )
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
            modifier = Modifier
                .focusRestorer()
                .focusGroup(),
        ) {
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
        }
    }
}

开始运行

现在,焦点小组标签页中的行会恢复焦点,您可以按以下步骤进行尝试:

  1. 选择焦点小组标签页
  2. 将焦点移至第 1 张卡片
  3. 按 Tab 键将焦点移至第 4 张卡片
  4. 按 up 方向键将焦点移至第 1 张卡片
  5. 按 Tab 键

当键盘焦点进入设置为行的焦点小组时,focusRestorer 修饰符会保存第 4 张卡片的引用,并在焦点进入该组时自动将焦点移至该卡片。

GIF 动画显示,当键盘焦点重新进入某行时,键盘焦点会移至该行之前选中的卡片。

图 15. 先按向上方向键,再按 Tab 键,焦点会返回到第 4 张卡片。

10. 编写测试

您可以通过测试来测试已实现的键盘焦点管理功能。Compose 提供了一套 API,可用于测试界面元素是否获得焦点,以及对界面组件执行按键操作。如需了解详情,请参阅在 Jetpack Compose 中进行测试 Codelab。

测试“焦点目标”标签页

在上一节中,您修改了 FocusTargetTab 可组合函数,将第 2 张卡片设置为焦点目标。接下来,我们将编写一个测试用例,用于验证上一节中手动实现的功能。编写测试的步骤如下:

  1. 打开 FocusTargetTabTest.kt。您将按照以下步骤修改 testSecondCardIsFocusTarget 函数。
  2. 通过对第 1 张卡片的 SemanticsNodeInteraction 对象调用 requestFocus 方法,请求将焦点移至第 1 张卡片。
  3. 使用 assertIsFocused() 方法确认第 1 张卡片已获得焦点。
  4. 在传递给 performKeyInput 方法的 lambda 中,使用 Key.Tab 值调用 pressKey 方法,来执行 Tab 键按下操作。
  5. 对第 2 张卡片的 SemanticsNodeInteraction 对象调用 assertIsFocused() 方法,测试键盘焦点是否会移至第 2 张卡片。

更新后的代码如下所示:

@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun testSecondCardIsFocusTarget() {
    composeTestRule.setContent {
        LocalInputModeManager
            .current
            .requestInputMode(InputMode.Keyboard)
        FocusTargetTab(onClick = {})
    }
    val context = InstrumentationRegistry.getInstrumentation().targetContext

    // Ensure the 1st card is focused
    composeTestRule
        .onNodeWithText(context.getString(R.string.first_card))
        .requestFocus()
        .performKeyInput { pressKey(Key.Tab) }

    // Test if focus moves to the 2nd card from the 1st card with Tab key
    composeTestRule
        .onNodeWithText(context.getString(R.string.second_card))
        .assertIsFocused()
}

开始运行

您可以通过点击 FocusTargetTest 类声明左侧显示的三角形图标来运行测试。如需了解详情,请参阅在 Android Studio 中测试中的运行测试部分。

Android Studio 显示一个上下文菜单,用于运行“FocusTargetTabTest”。

11. 恭喜

祝贺您!您已经掌握了键盘焦点管理的核心要素:

  • 焦点目标
  • 焦点遍历

您可以使用以下 Compose 修饰符来控制焦点遍历顺序:

  • focusGroup 修饰符
  • focusProperties 修饰符

您结合硬件键盘交互、初始焦点以及焦点恢复功能,实现了典型的用户体验模式。这些模式是通过组合以下 API 实现的:

  • FocusRequester 类
  • focusRequester 修饰符
  • focusRestorer 修饰符
  • LaunchedEffect 可组合函数

您可以使用插桩测试来测试已实现的用户体验。Compose 提供了一些方法来执行按键操作,并测试 SemanticsNode 是否获得了键盘焦点。

了解详情