更改焦点行为

有时,您需要替换屏幕上元素的默认焦点行为。例如,您可能想要对可组合项进行分组防止将焦点置于某个可组合项上、在某个可组合项上明确请求焦点捕获或释放焦点,或者在进入或退出时重定向焦点。本部分介绍了在默认值无法满足需求时如何更改焦点行为。

为焦点小组提供一致的导航方式

有时,Jetpack Compose 不会立即猜测标签页式导航的下一项,尤其是在标签页和列表等复杂的父级 Composables 出现时。

虽然焦点搜索通常遵循 Composables 的声明顺序,但在某些情况下不可能这样做,例如当层次结构中的某个 Composables 是不完全可见的水平滚动项时。详见下例。

Jetpack Compose 可能会决定将焦点放在最接近屏幕开头的位置,如下所示,而不是按照您希望的单向导航路径继续:

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

在此示例中,很明显,开发者并不打算让焦点从“巧克力”标签页跳到下面的第一张图片,然后再返回到“糕点”标签页。相反,他们希望将焦点一直放在标签页上,直到最后一个标签页上,然后专注于内部内容:

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

如果一组可组合项必须依序获得焦点(例如上一个示例的 Tab 行),您需要将 Composable 封装在具有 focusGroup() 修饰符的父项中:

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

双向导航会查找与给定方向最接近的可组合项。如果另一组中的某个元素比当前组中非完全可见的项更近,导航会选择距离最近的可组合项。为避免此行为,您可以应用 focusGroup() 修饰符。

FocusGroup 会使整个组在焦点方面看起来像单个实体,但该组本身不会获得焦点,而是会获得距离最近的子项并获得焦点。这样,导航就知道要先转到非完全可见的项,然后再离开组。

在这种情况下,即使 SweetsCards 对用户完全可见,且部分 FilterChip 可能处于隐藏状态,FilterChip 的三个实例也会在 SweetsCard 项之前获得焦点。之所以发生这种情况,是因为 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 property 使 Composable 不可聚焦。

var checked by remember { mutableStateOf(false) }

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

使用 FocusRequester 请求键盘焦点

在某些情况下,您可能需要明确请求焦点,以响应用户互动。例如,您可以询问用户是否想要重新开始填写表单,如果用户按下“是”,您希望重新聚焦该表单的第一个字段。

首先需要将 FocusRequester 对象与您要将键盘焦点移至的可组合项相关联。在以下代码段中,通过设置名为 Modifier.focusRequester 的修饰符将 FocusRequester 对象与 TextField 相关联:

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. 一个屏幕动画,显示了两列并排放置的按钮,并以动画方式将焦点从一列移到另一列

在我们深入介绍如何创建该对象之前,请务必先了解焦点搜索的默认行为。如果不进行任何修改,当焦点搜索到达 Clickable 3 项后,按方向键上的 DOWN(或等效箭头键)会将焦点移到 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 1 中的 Clickable 3 Composable 后,下一个获得焦点的项即为另一个 Column 中的 Clickable 4。可通过将 focusDirectionfocusProperties 修饰符内的 enterexit 值结合使用来实现此行为。它们都需要一个 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() 函数会将焦点移到指定项,或将焦点移到函数参数中隐含的方向。