更改焦点行为

有时需要覆盖屏幕上元素的默认焦点行为。例如,您可能希望组合可组合项阻止某个可组合项获得焦点、显式请求某个可组合项的焦点、捕获或释放焦点,或在进入或退出时重定向焦点。本节介绍如何在默认设置不符合您的需求时更改焦点行为。

使用焦点组提供一致的导航

有时,Jetpack Compose不会立即猜测选项卡导航的下一个正确项,尤其是在标签和列表等复杂的父级Composables参与时。

虽然焦点搜索通常遵循Composables的声明顺序,但在某些情况下这是不可能的,例如,当层次结构中的Composables之一是不可完全看见的水平可滚动项时。以下示例显示了这种情况。

Jetpack Compose可能会决定将焦点放在屏幕开头最接近的项目上,如下所示,而不是沿着您期望的单向导航路径继续。

Animation of an app showing a top horizontal navigation and a list of items below.
图1. 显示顶部水平导航和下方项目列表的应用动画

在此示例中,很明显,开发人员并不打算将焦点从“巧克力”选项卡跳转到下面的第一个图像,然后再跳回到“糕点”选项卡。相反,他们希望焦点继续停留在选项卡上,直到最后一个选项卡,然后才将焦点放在内部内容上。

Animation of an app showing a top horizontal navigation and a list of items below.
图2. 显示顶部水平导航和下方项目列表的应用动画

在需要按顺序获取一组可组合项焦点的场景中(例如,前面示例中的选项卡行),您需要将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使整个组在焦点方面看起来像单个实体,但组本身不会获得焦点——而是最接近的子项将获得焦点。通过这种方式,导航知道在离开组之前先转到不可完全看见的项。

在这种情况下,在SweetsCard项之前,将先聚焦FilterChip的三个实例,即使SweetsCards对用户完全可见,并且某些FilterChip可能隐藏。发生这种情况是因为focusGroup修饰符告诉焦点管理器调整聚焦项的顺序,以便导航更容易,并且与UI更一致。

如果没有focusGroup修饰符,如果FilterChipC不可见,焦点导航将最后选择它。但是,添加此类修饰符不仅使其可发现,而且还将在FilterChipB之后立即获取焦点,正如用户所期望的那样。

使可组合项可聚焦

某些可组合项在设计上是可聚焦的,例如按钮或附加了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 属性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对象与您要将键盘焦点移动到的可组合项相关联。在以下代码片段中,FocusRequester对象通过设置名为Modifier.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()
)

在进入或退出时重定向焦点

有时,您需要提供非常具体的导航类型,如下面的动画所示。

Animation of a screen showing two columns of buttons placed side by side and animating focus from one column to the other.
图3. 显示并排放置的两列按钮并从一列动画到另一列焦点的屏幕动画

在我们深入探讨如何创建它之前,了解焦点搜索的默认行为非常重要。在没有任何修改的情况下,一旦焦点搜索到达Clickable 3项目,按下D-Pad上的DOWN键(或等效的箭头键)会将焦点移动到Column下方显示的内容,退出该组并忽略右侧的项目。如果没有可聚焦的项目可用,焦点不会移动到任何地方,而是停留在Clickable 3上。

要更改此行为并提供预期的导航,您可以利用focusProperties修饰符,它可以帮助您管理焦点搜索进入或退出Composable时发生的情况。

val otherComposable = remember { FocusRequester() }

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

可以将焦点定向到特定的Composable,无论何时它进入或退出层次结构的某一部分——例如,当您的UI有两个列并且您希望确保每当处理第一个列时,焦点切换到第二个列。

Animation of a screen showing two columns of buttons placed side by side and animating focus from one column to the other.
图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()函数将焦点推进到指定的项目,或函数参数中暗示的方向。