有时需要替换屏幕上元素的默认焦点行为。例如,您可能需要对可组合项进行分组、阻止对某个可组合项的焦点、明确地请求对某个可组合项的焦点、捕获或释放焦点,或者在进入或退出时重定向焦点。本部分介绍了在默认行为不符合您的需求时如何更改焦点行为。
使用焦点组提供连贯的导航
有时,Jetpack Compose 无法立即猜测选项卡式导航的正确下一个项目,尤其是在涉及选项卡和列表等复杂的父级 Composables
时。
虽然焦点搜索通常遵循 Composables
的声明顺序,但在某些情况下这是不可能的,例如当层次结构中的某个 Composable
是水平可滚动且未完全可见时。下面的示例对此进行了说明。
Jetpack Compose 可能会决定聚焦屏幕起始处最近的下一个项目,如下所示,而不是按照您期望的单向导航路径继续进行

在此示例中,开发人员显然不希望焦点从“巧克力”标签页跳到下面的第一个图片,然后又跳回“糕点”标签页。相反,他们希望焦点继续停留在标签页上,直到最后一个标签页,然后聚焦内部内容

在需要一组可组合项按顺序获取焦点的情况下,例如上一个示例中的 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
使整个组在焦点方面显示为单个实体,但组本身不会获得焦点,而是由最近的子项获得焦点。这样,导航就知道在离开组之前转到未完全可见的项目。
在这种情况下,三个 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 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() ) |
进入或退出时重定向焦点
有时,您需要提供一种非常具体的导航方式,例如以下动画中所示的方式

在我们深入探讨如何创建此功能之前,了解焦点搜索的默认行为非常重要。在没有任何修改的情况下,一旦焦点搜索到达 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
,无论它何时进入或退出层次结构的某个部分——例如,当您的界面有两列时,您希望确保每当第一列处理完毕时,焦点就切换到第二列

在此 GIF 中,一旦焦点到达 Column
1 中的 Clickable 3 Composable
,下一个获得焦点的项目将是另一个 Column
中的 Clickable 4
。通过将 focusDirection
与 focusProperties
修饰符内部的 enter
和 exit
值结合,可以实现此行为。它们都需要一个 lambda,该 lambda 以焦点来源方向作为参数并返回一个 FocusRequester
。此 lambda 可以有三种不同的行为:返回 FocusRequester.Cancel
会阻止焦点继续;FocusRequester.Default
不会改变其行为。而提供附属于另一个 Composable
的 FocusRequester
会使焦点跳到该特定的 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()
函数将焦点前进到指定的项目,或函数参数中指示的方向。
为您推荐
- 注意:当 JavaScript 关闭时,会显示链接文本
- 响应焦点
- Compose 中的焦点
- 更改焦点遍历顺序