更改焦点行为

有时,您需要覆盖元素的默认焦点行为 。例如,您可能想要对可组合项进行分组,从而防止 将焦点置于某个可组合项上,明确请求将焦点置于某个可组合项上, 捕获或释放焦点,或者在进入或退出时重定向焦点。本次 部分介绍了如何在默认操作并非默认操作时更改焦点行为 。

通过焦点小组提供连贯一致的导航

有时,Jetpack Compose 不会立即猜测正确的下一项 标签页式导航,尤其是在复杂的父级 Composables(例如标签页和 清单

虽然焦点搜索通常遵循 Composables 的声明顺序, 但在某些情况下,这是不可行的,例如,当Composables 是不完全可见的水平可滚动项。显示位置 请参阅下面的示例。

Jetpack Compose 可能会决定聚焦于最接近 而不是继续沿途的 单向导航:

应用的动画,顶部水平导航栏在下方显示项目列表。
图 1. 应用的动画,顶部水平导航栏在下方显示项目列表

在本例中,很明显,开发者并未打算 请从巧克力标签跳转到以下第一张图片,然后返回到 糕点标签页。他们希望将重点放在标签页上, 最后一个标签页,然后重点关注内部内容:

应用的动画,顶部水平导航栏在下方显示项目列表。
图 2. 应用的动画,顶部水平导航栏在下方显示项目列表

在必须让一组可组合项获得焦点的情况下 就像上一个示例中的 Tab 行一样,您需要将 具有 focusGroup() 修饰符的父项中的 Composable

LazyVerticalGrid(columns = GridCells.Fixed(4)) {
    item(span = { GridItemSpan(maxLineSpan) }) {
        Row(modifier = Modifier.focusGroup()) {
            FilterChipA()
            FilterChipB()
            FilterChipC()
        }
    }
    items(chocolates) {
        SweetsCard(sweets = it)
    }
}

双向导航会查找距离给定的距离最近的可组合项 方向 - 如果另一组元素中的元素比不完全可见的元素更近 项,则导航会选择距离最近的项。为了避免这种情况 您可以应用 focusGroup() 修饰符。

FocusGroup 让整个群组在焦点方面看起来就像一个实体, 但该组本身不会获得焦点,而最近的子组会获得焦点 获得焦点。这样,导航就知道要转到非完全可见的 然后才能退出群组。

在本例中,FilterChip 的三个实例将在 SweetsCard 项即使SweetsCards对 用户和部分FilterChip可能已被隐藏。这是因为 focusGroup 修饰符告知焦点管理器调整项的顺序 使导航更轻松且与界面更一致。

在不使用 focusGroup 修饰符的情况下,如果 FilterChipC 不可见,则聚焦于 则会最后选择该类别。不过,添加此类修饰符会使它 仅可被发现,但会在 FilterChipB之后立即获得焦点,因为 满足您的需求

使可组合项可聚焦

某些可组合项在设计上可聚焦,例如 Button 或采用 clickable 修饰符。如果您希望将 可组合项的可聚焦行为,可以使用 focusable 修饰符:

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

使可组合项不可聚焦

在某些情况下,您的某些元素可能不应参与 在极少数情况下,您可以利用canFocus propertyComposable 从可聚焦中排除。

var checked by remember { mutableStateOf(false) }

Switch(
    checked = checked,
    onCheckedChange = { checked = it },
    // Prevent component from being focused
    modifier = Modifier
        .focusProperties { canFocus = false }
)

使用 FocusRequester 请求键盘焦点

在某些情况下,您可能需要明确请求焦点,以作为对 用户互动例如,您可以询问用户是否想要重启设备 如果用户点击“是”您想要重新聚焦第一个字段 这种形式。

首先需要将 FocusRequester 对象与 可组合项。在以下代码中 在代码段中,FocusRequester 对象会通过设置TextField 称为 Modifier.focusRequester 的修饰符:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

您可以调用 FocusRequester 的 requestFocus 方法来发送实际的焦点请求。您应该在 Composable 上下文之外调用此方法 (否则,每次重组时都会重新执行该函数)。以下代码段 显示了如何请求系统在按下按钮时移动键盘焦点 已点击:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Button(onClick = { focusRequester.requestFocus() }) {
    Text("Request focus on TextField")
}

获取和释放焦点

您可以利用重点来引导用户提供正确的应用数据 (例如,获取有效的电子邮件地址或电话号码), 数字。虽然错误状态可让您的用户了解所发生的情况, 可能需要包含错误信息的字段保持专注,直到 已修复。

为了捕获焦点,您可以调用 captureFocus() 方法,并且 之后请改用 freeFocus() 方法将其释放,如下所示 示例:

val textField = FocusRequester()

TextField(
    value = text,
    onValueChange = {
        text = it

        if (it.length > 3) {
            textField.captureFocus()
        } else {
            textField.freeFocus()
        }
    },
    modifier = Modifier.focusRequester(textField)
)

焦点修饰符的优先级

Modifiers 可视为仅有一个子元素的元素,因此当您将 左侧(或顶部)的每个 Modifier 都会封装紧随其后的 Modifier 右侧(或下方)。这意味着,第二个 Modifier 包含在 第一个,以便在声明两个 focusProperties 时, 一个实例能起作用,因为下面这几个代码包含在最顶层。

如需进一步阐明此概念,请查看以下代码:

Modifier
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

在本例中,将 item2 指示为右侧焦点的 focusProperties 将 因为它包含在前一个网址中;因此,item1 将是 其中一个。

利用此方法,父级发布商还可以将行为重置为默认值, 使用 FocusRequester.Default

Modifier
    .focusProperties { right = Default }
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

父级不必是同一修饰符链的一部分。家长 可组合项可以覆盖子级可组合项的焦点属性。例如: 请考虑使用会让该按钮无法聚焦的 FancyButton

@Composable
fun FancyButton(modifier: Modifier = Modifier) {
    Row(modifier.focusProperties { canFocus = false }) {
        Text("Click me")
        Button(onClick = { }) { Text("OK") }
    }
}

用户可以通过将 canFocus 设置为 true 来重新将此按钮设为可聚焦:

FancyButton(Modifier.focusProperties { canFocus = true })

与每个 Modifier 一样,与焦点相关的项的行为会因顺序而异 您需要声明它们。例如,如下代码会使 Box 可聚焦,但 FocusRequester 与此可聚焦对象没有关联,因为它 在可聚焦对象之后声明。

Box(
    Modifier
        .focusable()
        .focusRequester(Default)
        .onFocusChanged {}
)

请务必注意,focusRequester 与第一个 位于层次结构中的其下方,因此此 focusRequester 指向 第一个可聚焦子项。如果没有可用的提供方,则它不会指向任何内容。 不过,由于 Box 可聚焦(得益于 focusable() 修饰符), 则可以使用双向导航进行导航

再举一例,由于 onFocusChanged() 修饰符是指出现在 focusable()focusTarget() 修饰符。

Box(
    Modifier
        .onFocusChanged {}
        .focusRequester(Default)
        .focusable()
)
Box(
    Modifier
        .focusRequester(Default)
        .onFocusChanged {}
        .focusable()
)

进入或退出时重定向焦点

有时,您需要提供一种非常具体的导航栏,比如 如以下动画所示:

一个屏幕的动画,其中显示两列并排放置的按钮,并在一列间以动画形式呈现焦点。
图 3. 一个屏幕的动画,其中有两列按钮并排放置,并且每列的焦点都以动画形式呈现

在我们深入探究如何创建之前,请务必先了解默认 焦点搜索行为无需任何修改,焦点搜索 按方向键上的 DOWN(或等效键)可看到 Clickable 3 项 箭头键)可将焦点移至 Column 下方显示的任何内容, 退出群组并忽略右边的群组如果没有 有可聚焦项可用,焦点不会移到任何位置,但始终位于 Clickable 3

若要改变这种行为并提供预期的导航,您可以利用 focusProperties 修饰符,可帮助您管理聚焦时会发生什么情况 进入或退出 Composable 时:

val otherComposable = remember { FocusRequester() }

Modifier.focusProperties {
    exit = { focusDirection ->
        when (focusDirection) {
            Right -> Cancel
            Down -> otherComposable
            else -> Default
        }
    }
}

每当进入特定的 Composable 时,都可以将焦点定向到它 或退出层次结构的特定部分 - 例如,当您的界面有两个 因此,您需要确保每当系统处理第一列时 焦点切换到第二个:

一个屏幕的动画,其中显示两列并排放置的按钮,并在一列间以动画形式呈现焦点。
图 4. 一个屏幕的动画,其中有两列按钮并排放置,并且每列的焦点都以动画形式呈现

在此 GIF 中,当焦点到达 Column 中的 Clickable 3 Composable 后,即 1, 获得焦点的下一个项目是另一个 Column 中的 Clickable 4。此行为 可以通过将 focusDirectionenterexit 结合使用来实现 focusProperties 修饰符内的值。它们都需要一个 lambda, 作为参数,表示焦点的来源方向,并返回 FocusRequester。此 lambda 有三种不同的行为方式:返回 FocusRequester.Cancel 会使焦点停止继续,而 FocusRequester.Default 不会改变其行为。而是提供 附加到另一个 ComposableFocusRequester 会使焦点跳至该位置 特定的 Composable

更改焦点推进方向

如需将焦点推进到下一项或朝某个精确的方向,您可以 利用 onPreviewKey 修饰符,并将 LocalFocusManager 表示为 使用 moveFocus 修饰符推进焦点。

以下示例展示了焦点机制的默认行为: 检测到tab按键,焦点移至焦点中的下一个元素 列表。虽然您通常不需要进行配置,但请务必 了解系统的内部运作原理 行为

val focusManager = LocalFocusManager.current
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.onPreviewKeyEvent {
        when {
            KeyEventType.KeyUp == it.type && Key.Tab == it.key -> {
                focusManager.moveFocus(FocusDirection.Next)
                true
            }

            else -> false
        }
    }
)

在此示例中,focusManager.moveFocus() 函数会将焦点提升到 指定的项,或者是指向函数参数中隐含的方向的。