对焦点目标进行分组

您可以使用 focusGroup 修饰符将多个焦点目标分组在一起。 通过分组,您可以管理焦点层次结构、隔离逻辑界面区域(例如工具栏、对话框操作或卡片列表),并在焦点进入或退出区域时进行拦截。

焦点小组概念:进入、退出和默认遍历

焦点群组会在其子焦点目标周围建立一个连贯的导航边界。了解焦点如何跨越此边界取决于两个关键概念:

  • Enter:当焦点从焦点组外的任何元素过渡到组内的元素时发生的事件。
  • 退出:当焦点离开组的最后一个内部元素并过渡到外部焦点目标时发生的事件。
显示焦点组边界上的进入和退出过渡的示意图。
图 1. 包含进入和退出事件的焦点小组。

默认焦点遍历行为

默认情况下,当焦点小组从外部接收焦点时:

  1. 进入群组:Compose 会自动选择一个内部子焦点目标。对于一维(标签页)导航,它会选择视觉外观顺序中的第一个子级。 对于二维(箭头键或方向键)导航,它会选择在几何上最接近传入方向的子级。
  2. 在群组内导航:重复按导航键可在群组所包含的同级焦点目标之间移动焦点。
  3. 退出群组:当用户导航到群组内最后一个焦点目标之后,下一次按键操作会退出群组,并将焦点移至群组外相邻的焦点目标。

自定义焦点进入重定向

在许多界面设计中,默认的入口目标(例如外观顺序中的第一个子级)对于用户而言并非最合乎逻辑或最方便的选择。

意图和使用情形

考虑使用确认对话框,其中左侧有一个“取消”按钮,右侧有一个主要“确认”按钮。当用户从表单字段向下导航到按钮栏时:

  • 默认行为:焦点会落在“取消”上,因为它是视觉外观顺序中的第一个按钮。
  • 预期行为:由于“确认”是主要推荐操作,因此您可能希望在从焦点顺序中较早的位置进入按钮组时,焦点直接落在“确认”上,同时仍允许用户在需要时按向左键选择“取消”。
按钮组的默认焦点进入。
图 2. 具有默认焦点入口目标的取消和确认按钮。

其他常见应用场景包括:

  • 在输入 TabRow 时,直接将焦点移至当前所选标签页。
  • 在进入媒体播放栏时,直接将焦点移至“播放 / 暂停”控件。

使用 onEnter 和 FocusRequester 实现

如需在进入焦点小组时重定向焦点,请执行以下操作:

  1. 将 focusGroup 修饰符应用于封装相关焦点目标的父容器。
  2. 将 focusProperties 修饰符应用于容器,并配置 onEnter 回调。
  3. 在 onEnter 回调(范围限定为 FocusEnterExitScope)中,使用 focusRequester 对与首选默认子级关联的 FocusRequester 调用 requestFocus()。

val defaultChildRequester = remember { FocusRequester() }

Row(
    modifier = Modifier
        .focusGroup()
        .focusProperties {
            // Intercept entry into this group and redirect focus to the primary action
            onEnter = {
                defaultChildRequester.requestFocus()
            }
        }
) {
    Button(onClick = { /* Cancel action */ }) {
        Text("Cancel")
    }
    Button(
        onClick = { /* Confirm action */ },
        modifier = Modifier.focusRequester(defaultChildRequester)
    ) {
        Text("Confirm (Default)")
    }
}

在 onEnter 和 onExit 中,您还可以调用 cancelFocusChange() 来完全阻止跨边界的焦点转换,或者检查 requestedFocusDirection 以根据用户是否使用 Tab、向下或向左键进行导航来自定义路由。

默认情况下,可滚动容器是焦点组

在 Jetpack Compose 中,LazyRow、LazyColumn 和 LazyVerticalGrid 等可滚动容器会自动充当焦点组。您无需明确向它们应用 focusGroup 修饰符即可设置 onEnter 或 onExit 回调。