有时,您需要替换屏幕上元素的默认焦点行为。例如,您可能想要对可组合项进行分组、防止将焦点置于某个可组合项上、在某个可组合项上明确请求焦点、捕获或释放焦点,或者在进入或退出时重定向焦点。本部分介绍了在默认值无法满足需求时如何更改焦点行为。
为焦点小组提供一致的导航方式
有时,Jetpack Compose 不会立即猜测标签页式导航的下一项,尤其是在标签页和列表等复杂的父级 Composables
出现时。
虽然焦点搜索通常遵循 Composables
的声明顺序,但在某些情况下不可能这样做,例如当层次结构中的某个 Composables
是不完全可见的水平滚动项时。详见下例。
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
会使整个组在焦点方面看起来像单个实体,但该组本身不会获得焦点,而是会获得距离最近的子项并获得焦点。这样,导航就知道要先转到非完全可见的项,然后再离开组。
在这种情况下,即使 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() ) |
在进入或退出时重定向焦点
有时,您需要提供一种非常具体的导航,如下方动画中所示的导航:
在我们深入介绍如何创建该对象之前,请务必先了解焦点搜索的默认行为。如果不进行任何修改,当焦点搜索到达 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,它将焦点来自的方向作为参数,并返回 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 中的焦点
- 更改焦点遍历顺序