官方文档链接:
https://developer.android.google.cn/develop/ui/compose/touch-input/focus?hl=zh-cn
1、更改焦点遍历顺序
1.1、替换一维遍历顺序
(1)创建焦点引用对象:
/// 创建4个引用对象(二选一)
// 语法1:
val (first, second, third, fourth) = remember { FocusRequester.createRefs() }
// 语法2:数组
val focusRequesters = remember { List(4){ FocusRequester.createRefs() } }
(2)使用 focusRequester 修饰符将它们分别与 可组合项
Column {
Row {
// 注意:TextButton 具有默认焦点(可聚焦)
// 如果此处是没有默认聚焦的元素,比如 Box,则需要在Modifer最后面(所有焦点函数之后)
// 注意是最后面加上 .focusable()
TextButton({}, Modifier.focusRequester(first/*focusRequesters[0]*/)/* .focusable()*/) { Text("First field") }
TextButton({}, Modifier.focusRequester(third/*focusRequesters[1]*/)) { Text("Third field") }
}
Row {
TextButton({}, Modifier.focusRequester(second/*focusRequesters[2]*/)) { Text("Second field") }
TextButton({}, Modifier.focusRequester(fourth/*focusRequesters[3]*/)) { Text("Fourth field") }
}
}
(3)使用 focusProperties 修饰符指定自定义遍历顺序
因为有些布局或组件默认会带有遍历默认顺序,比如 Column 的孩子的默认遍历顺序是从上到下。
但是有时候我们需要改变这种顺序:
// 一维:它的下一个是谁、上一个是谁
Modifier
.focusRequester(first)
.focusProperties { next = second }
....
/* .focusable()*/
...
// 二维:上下左右
Modifier
.focusRequester(fourth)
.focusProperties {
down = third
right = second
}
2 更改焦点行为
2.1 焦点小组
LazyVerticalGrid(columns = GridCells.Fixed(4)) {
item(span = { GridItemSpan(maxLineSpan) }) {
// 将其设置为一个焦点组:
Row(modifier = Modifier.focusGroup()) {
FilterChipA()
FilterChipB()
FilterChipC()
}
}
items(chocolates) {
SweetsCard(sweets = it)
}
}
2.2 使可组合项可聚焦
var color by remember { mutableStateOf(Green) }
Box(
Modifier
.background(color)
.onFocusChanged { color = if (it.isFocused) Blue else Green }
// 注意要放在焦点相关函数的最后面,
.focusable()
) {
Text("Focusable 1")
}
2.3 使可组合项不可聚焦
var checked by remember { mutableStateOf(false) }
Switch(
checked = checked,
onCheckedChange = { checked = it },
// Prevent component from being focused
modifier = Modifier
// 使原本可以聚焦的,变得不可聚焦...
.focusProperties { canFocus = false }
)
2.4 使用 FocusRequester 手动请求焦点
上面说了可以手动添加焦点引用对象。添加这个请求对象之后,可以使用焦点请求对象手动请求焦点…
// 焦点请求对象
val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
// 为这个元素添加上面的焦点请求对象(覆盖默认的焦点请求对象)
modifier = Modifier.focusRequester(focusRequester)
)
// 在添加焦点请求对象之后,就可以手动请求:
Button(onClick = {
// 手动请求(在.focusRequester(focusRequester)之后)
focusRequester.requestFocus()
}) {
Text("Request focus on TextField")
}
2.5 捕获和释放 焦点
调用 captureFocus() 方法,并且之后必须要用 freeFocus() 方法将其释放
// 经常用在 TextField 之类的组件
// 比如下面的示例中,首先 TextField 已经获取焦点,
// 但是我还想让它在输入长度达到 3 之后突出视觉效果,可以尝试捕获焦点…
val textField = FocusRequester()
TextField(
value = text,
onValueChange = {
text = it
if (it.length > 3) {
textField.captureFocus()
} else {
textField.freeFocus()
}
},
modifier = Modifier.focusRequester(textField)
)
2.6 焦点修饰符的优先级
Modifier
.focusProperties { right = Default }
.focusProperties { right = item1 }
.focusProperties { right = item2 }
// 放在最后...
.focusable()
注意:焦点相关修饰函数顺序由决定性作用,不同的顺序会带来不同的效果:
// 作用失效案例1:
Box(
Modifier
.focusable() // 让焦点函数生效
.focusRequester(Default)// 后声明请求器 → 无法关联
.onFocusChanged {}// 当焦点发生变化的时候,回调
// 上面这个案例会导致下面两个焦点修饰符函数不生效。
// 因为 focusable() 的作用的让(前面已经)配置生效。
// 所以先生效,再配置的逻辑顺序是不符合要求的
)
// 作用失效案例2:
Box(
Modifier
.onFocusChanged {}// 先监听焦点变化(无效),应该先绑定焦点
.focusRequester(Default)
.focusable()
)
// 正确顺序:
Box(
Modifier
.focusRequester(Default) // 先声明请求器
.onFocusChanged {} // 可放在此处(但可能不推荐)
.focusable() // 后声明可聚焦 → 请求器生效
)
2.7 进入或退出时重定向焦点
什么是进入:
一般就是 Enter 键触发的行为
什么是退出:退出可组合区域,比如说焦点离开Colum1,进入Column2。
离开Column1就是退出行为。
比如在第一列离开,如果想要将焦点放置在第二列,可以:
val otherComposable = remember { FocusRequester() }
Modifier.focusProperties {
// 离开第一列的时候,我们根据离开的方向指定下一个焦点应该到达何处
exit = { focusDirection ->
when (focusDirection) {
Right -> Cancel// 离开且按右方向键,就取消焦点
Down -> otherComposable// 离开且按下键,就移动到第二列
else -> Default// 其他方向使用默认
}
}
}
2.8 更改焦点推进方向
val focusManager = LocalFocusManager.current
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.onPreviewKeyEvent {
when {
// 检测到tab按键(并释放抬起),焦点移至焦点中的下一个元素 列表
KeyEventType.KeyUp == it.type && Key.Tab == it.key -> {
// 移动焦点到默认的下一个焦点元素
focusManager.moveFocus(FocusDirection.Next)
true
}
else -> false
}
}
)
3、回应焦点
3.1、添加视觉效果(比如颜色)
var color by remember { mutableStateOf(Color.White) }
Card(
modifier = Modifier
.onFocusChanged {
color = if (it.isFocused) Red else White
}
.border(5.dp, color)
) {}
3.2、高级视觉提示
(1)首先,创建一个 IndicationInstance,以在界面中直观地绘制所需提示:
private class MyHighlightIndicationNode(private val interactionSource: InteractionSource) :
Modifier.Node(), DrawModifierNode {
private var isFocused = false
override fun onAttach() {
coroutineScope.launch {
var focusCount = 0
interactionSource.interactions.collect { interaction ->
when (interaction) {
is FocusInteraction.Focus -> focusCount++
is FocusInteraction.Unfocus -> focusCount--
}
val focused = focusCount > 0
if (isFocused != focused) {
isFocused = focused
invalidateDraw()
}
}
}
}
override fun ContentDrawScope.draw() {
drawContent()
if (isFocused) {
drawRect(size = size, color = Color.White, alpha = 0.2f)
}
}
}
(2)接下来,创建一个 Indication 并记住聚焦状态:
object MyHighlightIndication : IndicationNodeFactory {
override fun create(interactionSource: InteractionSource): DelegatableNode {
return MyHighlightIndicationNode(interactionSource)
}
override fun hashCode(): Int = -1
override fun equals(other: Any?) = other === this
}
(3)通过 indication() 修饰符将 Indication 和 InteractionSource 添加到界面中:
var interactionSource = remember { MutableInteractionSource() }
Card(
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = MyHighlightIndication,
enabled = true,
onClick = { }
)
) {
Text("hello")
}
3.3、焦点状态
// 焦点一般有这三种状态,可以直接在焦点回调里面获取:
Modifier.onFocusChanged {
val isFocused = it.isFocused// 只检查当前元素
val hasFocus = it.hasFocus// 不仅检查当前,还检查孩子们
val isCaptured= it.isCaptured//每当获得焦点时,isCaptured 都会返回 true
// 一般出现这种清空都是当 TextField 包含不正确的数据时,就会尝试聚焦 其他元素不会清除焦点。
}