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

Jetpack Compose TvLazyColumn不同高度项的PivotOffset适配问题

Android TV Jetpack Compose:TvLazyColumn动态适配聚焦枢轴点解决不同行样式对齐问题

问题场景

我用TvLazyColumn实现了7个列表项,每个项包含标题和带可聚焦视频缩略图的TvLazyRow:

  • 6个为单行标题样式,标题与TvLazyRow的内边距统一
  • 1个为多行标题样式,包含LIVE徽章、标题行、信息行

需求是:通过D-pad上下滚动TvLazyColumn时,无论聚焦缩略图的垂直位置如何,每个列项的顶部都要与TvLazyColumn顶部对齐——也就是把列的聚焦枢轴点设在每个项的标题顶部,而非缩略图上。

遇到的卡点:
如果所有项都是单行标题,直接调整PivotOffset就能实现,但因为存在一个多行样式的项,固定偏移无法适配。尝试过根据焦点行类型动态切换PivotOffset,但偏移仅在滚动过项内的TvLazyRow后才生效,需要缩略图一获得焦点就立刻应用正确偏移。


解决方案:动态计算聚焦偏移并即时生效

核心思路是根据行类型主动计算聚焦偏移量,结合焦点请求器确保焦点切换时立即触发偏移更新,具体实现步骤如下:

1. 定义行类型枚举

先区分两种行样式,方便后续逻辑判断:

enum class RowType {
    SINGLE_LINE_HEADER,
    MULTI_LINE_HEADER
}

2. 动态计算枢轴偏移量

提前获取两种标题区域的高度(可通过固定样式计算,或用Modifier.onSizeChanged获取实际渲染高度),然后在TvLazyRow中根据行类型设置对应的PivotOffset。

3. 完整代码示例

@Composable
fun DynamicPivotTvLazyColumn() {
    // 示例:提前计算两种标题的高度(实际项目可通过onSizeChanged获取动态高度)
    val singleLineHeaderHeight = 48.dp // 单行标题+内边距总高度
    val multiLineHeaderHeight = 80.dp // 多行标题(徽章+标题+信息)+内边距总高度
    val thumbnailHeight = 120.dp // 缩略图固定高度

    TvLazyColumn(
        modifier = Modifier.fillMaxSize(),
        // 全局默认枢轴,后续会被item的pivotOffsets覆盖
        pivotOffsets = PivotOffset(parentFraction = 0f, childFraction = 0f)
    ) {
        // 6个单行标题项
        items(6) { index ->
            RowItem(
                rowType = RowType.SINGLE_LINE_HEADER,
                headerHeight = singleLineHeaderHeight,
                thumbnailHeight = thumbnailHeight,
                title = "单行标题 $index",
                items = List(10) { "缩略图 $it" }
            )
        }

        // 1个多行标题项
        item {
            RowItem(
                rowType = RowType.MULTI_LINE_HEADER,
                headerHeight = multiLineHeaderHeight,
                thumbnailHeight = thumbnailHeight,
                title = "LIVE 热门赛事直播",
                subInfo = "正在直播:欧冠小组赛",
                items = List(10) { "直播缩略图 $it" }
            )
        }
    }
}

@Composable
fun RowItem(
    rowType: RowType,
    headerHeight: Dp,
    thumbnailHeight: Dp,
    title: String,
    subInfo: String? = null,
    items: List<String>
) {
    val focusRequester = remember { FocusRequester() }
    val density = LocalDensity.current
    // 将dp转换为像素,用于计算偏移比例
    val headerHeightPx = with(density) { headerHeight.toPx() }
    val thumbnailHeightPx = with(density) { thumbnailHeight.toPx() }

    Column(modifier = Modifier.fillMaxWidth()) {
        // 渲染标题区域
        when (rowType) {
            RowType.SINGLE_LINE_HEADER -> {
                Text(
                    text = title,
                    style = MaterialTheme.typography.headlineSmall,
                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
                )
            }
            RowType.MULTI_LINE_HEADER -> {
                Row(
                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Box(
                        modifier = Modifier
                            .background(Color.Red)
                            .padding(horizontal = 6.dp, vertical = 2.dp),
                        contentAlignment = Alignment.Center
                    ) {
                        Text(text = "LIVE", color = Color.White, fontSize = 12.sp)
                    }
                    Spacer(modifier = Modifier.width(8.dp))
                    Column {
                        Text(text = title, style = MaterialTheme.typography.headlineSmall)
                        Text(text = subInfo ?: "", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
                    }
                }
            }
        }

        // 带缩略图的TvLazyRow,核心设置动态pivotOffsets
        TvLazyRow(
            modifier = Modifier
                .padding(start = 16.dp, bottom = 16.dp)
                .focusRequester(focusRequester),
            pivotOffsets = PivotOffset(
                parentFraction = 0f,
                // 计算子项枢轴比例:让行项顶部对齐TvLazyColumn顶部
                childFraction = headerHeightPx / (headerHeightPx + thumbnailHeightPx)
            )
        ) {
            items(items) { item ->
                Box(
                    modifier = Modifier
                        .size(180.dp, thumbnailHeight)
                        .background(Color.DarkGray)
                        .focusable(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = item, color = Color.White)
                }
            }
        }

        // 确保焦点进入时立即应用偏移
        LaunchedEffect(Unit) {
            focusRequester.requestFocus()
        }
    }
}

关键说明

  • 动态计算childFraction:该参数是聚焦子项(缩略图)的枢轴点相对于自身高度的比例,通过标题高度/(标题高度+缩略图高度)计算,能保证聚焦时行项顶部刚好与TvLazyColumn顶部对齐。
  • FocusRequester即时触发:通过LaunchedEffect请求焦点,避免偏移在滚动后才生效的问题,确保焦点切换到该行时立刻应用正确的枢轴设置。
  • 动态高度适配:如果标题高度是动态的(比如文本自动换行),可以给标题区域添加Modifier.onSizeChanged获取实际渲染高度,再更新pivotOffsets。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 19:05:34