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

Android TV Compose:如何结合初始焦点恢复与popBackStack导航焦点恢复?

Android TV Compose 实现初始+导航返回的焦点恢复方案

问题根源

JetStream示例中的createInitialFocusRestorerModifiers()仅处理页面首次进入的焦点逻辑,未适配导航popBackStack()场景;同时TV Lazy列表会回收滚出视图的项,导致对应的FocusRequester实例被销毁,返回时直接调用已销毁的请求器就会触发「FocusRequester is not initialized」错误。

解决方案

核心思路是保存焦点项的唯一标识而非直接保存FocusRequester,在页面进入(含导航返回)时,通过标识定位目标项,确保项可见后再请求焦点。

1. 实现焦点状态管理器

用SavedStateHandle持久化焦点项的唯一key,同时管理FocusRequester的注册与注销:

class TvFocusRestorer(
    private val savedStateHandle: SavedStateHandle,
    private val focusKey: String = "tv_selected_item_key"
) {
    var selectedItemKey: String? by savedStateHandle.saveable { mutableStateOf(null) }
    private val focusRequesterMap = mutableMapOf<String, FocusRequester>()
    private val coroutineScope = CoroutineScope(Dispatchers.Main)

    fun requestFocusForKey(lazyListState: LazyListState) {
        selectedItemKey?.let { targetKey ->
            // 先检查目标项是否在可见区域
            val visibleIndex = lazyListState.layoutInfo.visibleItemsIndices.firstOrNull { index ->
                lazyListState.layoutInfo.visibleItems[index].key == targetKey
            }

            if (visibleIndex != null) {
                coroutineScope.launch {
                    delay(50) // 延迟确保项完成重组
                    focusRequesterMap[targetKey]?.requestFocus()
                }
            } else {
                // 目标项不在可见区,先滚动到对应位置
                val targetIndex = lazyListState.layoutInfo.totalItemsCount.takeIf { it > 0 }?.let { total ->
                    (0 until total).firstOrNull { index ->
                        lazyListState.layoutInfo.items[index].key == targetKey
                    }
                } ?: 0

                coroutineScope.launch {
                    lazyListState.scrollToItem(targetIndex)
                    delay(50)
                    focusRequesterMap[targetKey]?.requestFocus() ?: lazyListState.focusRequester.requestFocus()
                }
            }
        } ?: run {
            // 无保存状态,聚焦首个项
            coroutineScope.launch {
                lazyListState.scrollToItem(0)
                delay(50)
                lazyListState.focusRequester.requestFocus()
            }
        }
    }

    fun registerFocusRequester(key: String, focusRequester: FocusRequester) {
        focusRequesterMap[key] = focusRequester
    }

    fun unregisterFocusRequester(key: String) {
        focusRequesterMap.remove(key)
    }
}

2. 在Lazy列表中集成焦点恢复

给每个列表项绑定唯一key,管理焦点状态的保存与恢复:

@Composable
fun TvContentRow(
    navController: NavHostController,
    savedStateHandle: SavedStateHandle,
    contentList: List<ContentItem>
) {
    val focusRestorer = remember { TvFocusRestorer(savedStateHandle) }
    val lazyListState = rememberLazyListState()

    // 页面初始化/返回时触发焦点恢复
    LaunchedEffect(Unit) {
        focusRestorer.requestFocusForKey(lazyListState)
    }

    TvLazyRow(
        state = lazyListState,
        modifier = Modifier.focusRequester(lazyListState.focusRequester)
    ) {
        items(
            items = contentList,
            key = { item -> item.id } // 必须设置唯一key,确保项识别准确
        ) { item ->
            val focusRequester = remember { FocusRequester() }

            // 注册/注销FocusRequester,避免内存泄漏
            DisposableEffect(item.id) {
                focusRestorer.registerFocusRequester(item.id, focusRequester)
                onDispose {
                    focusRestorer.unregisterFocusRequester(item.id)
                }
            }

            ContentItemCard(
                item = item,
                modifier = Modifier
                    .focusRequester(focusRequester)
                    .onFocusChanged {
                        if (it.isFocused) {
                            focusRestorer.selectedItemKey = item.id
                        }
                    },
                onClick = { navController.navigate("detail/${item.id}") }
            )
        }
    }
}

关键注意事项

  • 务必给每个Lazy项设置唯一key,保证列表重组时能精准定位目标项
  • 用DisposableEffect管理FocusRequester的生命周期,防止内存泄漏
  • 滚动后添加短延迟再请求焦点,确保项已完成重组和请求器初始化
  • 当目标项不在可见区域时,先滚动到对应位置再触发焦点请求

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 05:07:47