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

实现类Google Home TV平滑滚动及TvLazy系列组件崩溃修复

解决方案:TvLazyColumn嵌套TvLazyRow的崩溃修复与Google Home式滚动实现

一、快速滚动崩溃问题修复

针对你遇到的visitAncestors called on an unattached node和Place was called on a node which was placed already崩溃,核心原因是快速滚动时Lazy列表项被频繁回收复用,导致Compose节点状态不一致,以下是具体修复方案:

1. 确保Lazy项的Key稳定唯一

给TvLazyColumn的每行、TvLazyRow的每个条目设置唯一且稳定的key,帮助Compose正确识别项的身份,减少不必要的节点销毁与重建:

TvLazyColumn(state = rememberTvLazyListState()) {
    items(rows, key = { row -> row.uniqueId }) { row ->
        TvLazyRow(
            state = rememberTvLazyListState(),
            modifier = Modifier.fillMaxWidth()
        ) {
            items(row.items, key = { item -> item.uniqueId }) { item ->
                // 条目布局
                ItemCard(item)
            }
        }
    }
}

2. 避免在未附着节点执行操作

如果自定义了焦点或布局相关逻辑,先判断节点是否处于活跃状态,比如使用FocusNode时:

val focusNode = remember { FocusNode() }
DisposableEffect(focusNode) {
    val job = launch {
        focusNode.requestFocus()
    }
    onDispose {
        job.cancel()
        // 确保节点回收时清理操作
        if (focusNode.isAttached) {
            focusNode.clearFocus()
        }
    }
}

3. 升级Compose TV依赖版本

这类节点状态异常的问题,很多已在新版androidx.tv库中修复,建议升级到最新稳定版:

implementation "androidx.tv:tv-material:1.0.0"
implementation "androidx.tv:tv-foundation:1.0.0"

二、实现Google Home Tab式滚动效果

Google Home的核心滚动逻辑是焦点移动时,列表平滑滚动跟随,焦点始终保持在可视区域内,具体实现如下:

1. 同步列滚动与行焦点

在TvLazyColumn中监听每行的焦点状态,当某行获得焦点时,自动滚动到该行并保持在屏幕合理位置:

val columnListState = rememberTvLazyListState()

TvLazyColumn(
    state = columnListState,
    // 设置滚动锚点,让焦点行保持在屏幕中间区域
    pivotOffsets = PivotOffsets(parentFraction = 0.5f)
) {
    itemsIndexed(rows, key = { _, row -> row.uniqueId }) { rowIndex, row ->
        val rowFocusRequester = remember { FocusRequester() }
        TvLazyRow(
            modifier = Modifier
                .focusRequester(rowFocusRequester)
                .focusProperties {
                    // 定义上下焦点遍历逻辑,避免焦点跳变
                    up = if (rowIndex > 0) FocusDirection.Up else FocusDirection.In
                    down = if (rowIndex < rows.size - 1) FocusDirection.Down else FocusDirection.In
                }
                .onFocusChanged { focusState ->
                    if (focusState.isFocused) {
                        LaunchedEffect(Unit) {
                            // 平滑滚动到当前行
                            columnListState.animateScrollToItem(rowIndex)
                        }
                    }
                }
        ) {
            // 行内条目布局
            RowItems(row.items, rowIndex)
        }
    }
}

2. 同步行滚动与条目焦点

在行内的TvLazyRow中,同样监听条目焦点,让行滚动跟随焦点条目:

@Composable
fun RowItems(items: List<Item>, rowIndex: Int) {
    val rowListState = rememberTvLazyListState()
    TvLazyRow(state = rowListState) {
        itemsIndexed(items, key = { _, item -> item.uniqueId }) { itemIndex, item ->
            ItemCard(
                item = item,
                modifier = Modifier
                    .onFocusChanged { focusState ->
                        if (focusState.isFocused) {
                            LaunchedEffect(Unit) {
                                // 滚动到当前条目,保持在可视区域
                                rowListState.animateScrollToItem(
                                    index = itemIndex,
                                    scrollOffset = rowListState.layoutInfo.viewportSize.width / 3
                                )
                            }
                        }
                    }
            )
        }
    }
}

3. 限制焦点移动步长

通过focusProperties自定义焦点遍历逻辑,避免焦点一次性跳过多个条目,确保滚动能跟上焦点速度:

ItemCard(
    item = item,
    modifier = Modifier
        .focusProperties {
            left = if (itemIndex > 0) FocusDirection.Left else FocusDirection.Enter
            right = if (itemIndex < items.size - 1) FocusDirection.Right else FocusDirection.Exit
        }
)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 17:25:18