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

Android Jetpack Compose TV 返回页面后如何恢复指定位置焦点?

解决方案

要解决返回页面时焦点和滚动状态不匹配的问题,无需为每个元素保存FocusRequester(避免OOM),只需保存滚动状态和最后聚焦的位置,返回后恢复滚动位置并自动聚焦到目标元素即可。

核心步骤

  1. 保存滚动状态:用rememberSaveable配合LazyListState.Saver保存TvLazyColumn和每个TvLazyRow的滚动状态,确保导航返回时列表位置不丢失。
  2. 记录聚焦位置:用rememberSaveable记录最后获得焦点的元素所在的行和列索引。
  3. 自动恢复焦点:返回列表页后,先滚动到目标行和列,再通过FocusRequester让目标元素自动获取焦点(仅为可见元素创建FocusRequester,避免内存占用过高)。

修改后的完整代码

class MainActivity : ComponentActivity() {

    private val rowItems = (0..20).toList()
    private val rows = (0..20).toList()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            MyAppNavHost(navController = navController)
        }
    }

    @Composable
    fun List(navController: NavController) {
        // 保存TvLazyColumn的滚动状态
        val columnState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() }
        // 保存每个TvLazyRow的滚动状态
        val rowStates = rememberSaveable { mutableMapOf<Int, LazyListState>() }
        // 记录最后聚焦的元素位置(行、列)
        val lastFocused = rememberSaveable { mutableStateOf(Pair(-1, -1)) }

        TvLazyColumn(
            modifier = Modifier.fillMaxSize(),
            state = columnState,
            verticalArrangement = Arrangement.spacedBy(16.dp),
            pivotOffsets = PivotOffsets(parentFraction = 0.05f),
        ) {
            items(rows.size) { rowPos ->
                Column() {
                    Text(text = "Row $rowPos")
                    // 获取或创建当前行的滚动状态
                    val rowState = rowStates.getOrPut(rowPos) {
                        rememberSaveable(saver = LazyListState.Saver) { LazyListState() }
                    }
                    TvLazyRow(
                        modifier = Modifier.height(70.dp),
                        state = rowState,
                        horizontalArrangement = Arrangement.spacedBy(16.dp),
                        pivotOffsets = PivotOffsets(parentFraction = 0.0f),
                    ) {
                        items(rowItems.size) { itemPos ->
                            var color by remember { mutableStateOf(Color.Green) }
                            val itemFocusRequester = remember { FocusRequester() }

                            Box(
                                Modifier
                                    .focusRequester(itemFocusRequester)
                                    .width(100.dp)
                                    .height(50.dp)
                                    .onFocusChanged {
                                        color = if (it.hasFocus) {
                                            // 更新最后聚焦的位置
                                            lastFocused.value = Pair(rowPos, itemPos)
                                            Color.Red
                                        } else {
                                            Color.Green
                                        }
                                    }
                                    .background(color)
                                    .clickable {
                                        navController.navigate("details")
                                    }
                            ) {
                                Text(text = "Item ${itemPos.toString()}", Modifier.align(Alignment.Center))
                            }

                            // 当返回列表页且当前元素是最后聚焦的目标时,请求焦点
                            LaunchedEffect(lastFocused.value, navController.currentDestination?.route) {
                                if (lastFocused.value == Pair(rowPos, itemPos) 
                                    && navController.currentDestination?.route == "list") {
                                    // 等待列表滚动稳定后请求焦点
                                    awaitFrame()
                                    itemFocusRequester.requestFocus()
                                }
                            }
                        }
                    }
                }
            }
        }

        // 初始加载或返回时,滚动到最后聚焦的位置
        LaunchedEffect(navController.currentBackStackEntry) {
            val (targetRow, targetItem) = lastFocused.value
            if (targetRow != -1 && targetItem != -1) {
                // 先滚动到目标行
                columnState.scrollToItem(targetRow)
                awaitFrame()
                // 再滚动到目标列
                rowStates[targetRow]?.scrollToItem(targetItem)
            } else {
                // 初始状态下聚焦到列表
                columnState.layoutInfo.visibleItemsInfo.firstOrNull()?.let {
                    columnState.scrollToItem(it.index)
                }
            }
        }
    }

    @Composable
    fun MyAppNavHost(
        navController: NavHostController = rememberNavController(),
        startDestination: String = "list"
    ) {
        NavHost(
            navController = navController,
            startDestination = startDestination
        ) {
            composable("details") { Details() }
            composable("list") { List(navController) }
        }
    }

    @Composable
    fun Details() {
        Box(
            Modifier
                .background(Color.Blue)
                .fillMaxSize()) {
            Text("Second Screen", Modifier.align(Alignment.Center), fontSize = 48.sp)
        }
    }
}

关键说明

  • 滚动状态保存:rememberSaveable配合LazyListState.Saver确保状态在配置变更或导航时不丢失,且仅保存必要的滚动信息,内存占用低。
  • 焦点请求优化:仅为当前可见的列表元素创建FocusRequester(用remember而非rememberSaveable),不可见元素的FocusRequester会被自动回收,避免OOM。
  • 延迟焦点请求:通过awaitFrame()等待列表滚动完成后再请求焦点,确保目标元素已被渲染,焦点请求生效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 18:54:58