You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Jetpack Compose中基于悬停事件动态切换子组件焦点?

Jetpack Compose实现自定义布局监听悬停并管理子组件焦点

核心方案

Jetpack Compose可以通过焦点请求器(FocusRequester)、**悬停事件监听(pointerInput)和组件位置收集(onPlaced)**组合实现需求:自定义布局自动收集所有可聚焦子组件的位置信息,监听自身范围内的悬停事件,根据坐标匹配目标子组件并触发焦点请求。

代码实现

1. 定义组件信息收集的CompositionLocal

用于在布局树中传递子组件信息的收集逻辑:

private val LocalComponentInfoCollector = compositionLocalOf<(Rect, FocusRequester) -> Unit> {
    { _, _ -> }
}

private data class ComponentInfo(val bounds: Rect, val focusRequester: FocusRequester)

2. 实现自定义布局TestLayout

负责监听悬停事件、收集子组件信息、匹配目标并请求焦点:

@Composable
fun TestLayout(content: @Composable () -> Unit) {
    val componentInfos = remember { mutableStateListOf<ComponentInfo>() }

    // 收集子组件的边界与焦点请求器
    val collectComponentInfo = { bounds: Rect, focusRequester: FocusRequester ->
        componentInfos.removeAll { it.focusRequester == focusRequester }
        componentInfos.add(ComponentInfo(bounds, focusRequester))
    }

    Layout(
        content = {
            CompositionLocalProvider(LocalComponentInfoCollector provides collectComponentInfo) {
                content()
            }
        },
        modifier = Modifier
            .pointerInput(Unit) {
                awaitPointerEventScope {
                    while (true) {
                        val event = awaitPointerEvent()
                        // 处理鼠标进入或移动事件
                        if (event.type == PointerEventType.Enter || event.type == PointerEventType.Move) {
                            val hoverPosition = event.changes.first().position
                            // 匹配悬停位置对应的子组件
                            val targetComponent = componentInfos.firstOrNull {
                                it.bounds.contains(hoverPosition)
                            }
                            targetComponent?.focusRequester?.requestFocus()
                        }
                    }
                }
            }
    ) { measurables, constraints ->
        // 通用测量与放置逻辑,适配任意子组件布局
        val placeables = measurables.map { it.measure(constraints) }
        val layoutWidth = placeables.maxOfOrNull { it.width } ?: constraints.minWidth
        val layoutHeight = placeables.sumOf { it.height }

        layout(layoutWidth, layoutHeight) {
            var yOffset = 0
            placeables.forEach { placeable ->
                placeable.placeRelative(0, yOffset)
                yOffset += placeable.height
            }
        }
    }
}

3. 实现可聚焦子组件示例

可聚焦按钮TestButton

自动向布局注册自身信息:

@Composable
fun TestButton(label: String) {
    val focusRequester = remember { FocusRequester() }
    val infoCollector = LocalComponentInfoCollector.current

    Button(
        onClick = {},
        modifier = Modifier
            .focusRequester(focusRequester)
            .focusable()
            .onPlaced { coordinates ->
                // 传递本地边界与焦点请求器
                infoCollector(coordinates.localBounds, focusRequester)
            }
    ) {
        Text(label)
    }
}

可聚焦文本框TestTextField

同样适配焦点管理逻辑:

@Composable
fun TestTextField() {
    val focusRequester = remember { FocusRequester() }
    val infoCollector = LocalComponentInfoCollector.current
    var text by remember { mutableStateOf("") }

    TextField(
        value = text,
        onValueChange = { text = it },
        modifier = Modifier
            .focusRequester(focusRequester)
            .focusable()
            .onPlaced { coordinates ->
                infoCollector(coordinates.localBounds, focusRequester)
            }
    )
}

LazyColumn可聚焦项

列表项也能自动加入焦点管理:

@Composable
fun TestLazyColumn() {
    val infoCollector = LocalComponentInfoCollector.current
    LazyColumn {
        items(10) { index ->
            val focusRequester = remember { FocusRequester() }
            Text(
                "列表项 $index",
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
                    .focusRequester(focusRequester)
                    .focusable()
                    .onPlaced { coordinates ->
                        infoCollector(coordinates.localBounds, focusRequester)
                    }
            )
        }
    }
}

4. 测试页面TestScreen

@Composable
fun TestScreen() {
    TestLayout {
        Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
                TestButton(label = "test1")
                TestButton(label = "test2")
            }
            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
                TestButton(label = "test3")
                TestButton(label = "test4")
            }
            TestTextField()
            TestLazyColumn()
        }
    }
}

关键注意事项

  • 仅添加focusable()修饰符的组件会被收集,自动过滤不可聚焦元素。
  • 组件大小或位置变化时,onPlaced会自动更新信息,确保悬停匹配准确性。
  • 若存在组件重叠,会取第一个匹配的组件,可根据需求调整遍历顺序或优先级逻辑。

内容的提问来源于stack exchange,提问作者mahabudhi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 11:13:15