如何在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
相关产品推荐
相关产品推荐

