为电视创建可滚动布局

对于 TV 应用,浏览体验依赖于高效的焦点导航。使用标准 Compose Foundation 延迟布局,您可以创建高性能的垂直和水平列表,这些列表会自动处理焦点驱动的滚动,以使活跃项保持在视图中。

针对电视优化的默认滚动行为

从 Compose Foundation 1.7.0 开始,标准延迟布局(例如 LazyRow 和 LazyColumn)内置了对焦点定位功能的支持。这是为 TV 应用构建目录的推荐方式,因为它有助于让获得焦点的项保持可见,并以直观的方式为用户定位。

如需实现基本的可滚动列表,请使用标准延迟组件。这些组件会自动处理方向键导航,并将焦点项显示在视图中。

import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items

@Composable
fun MovieCatalog(movies: List<Movie>) {
    LazyRow {
        items(movies) { movie ->
            MovieCard(
                movie = movie,
                onClick = { /* Handle click */ }
            )
        }
    }
}

使用 BringIntoViewSpec 自定义滚动行为

如果您的设计需要特定的“轴心”点(例如,让焦点项始终位于距离左边缘 30% 的位置),您可以使用 BringIntoViewSpec 自定义滚动行为。这取代了旧版 pivotOffsets 功能,让您可以精确定义视口应如何滚动以适应焦点项。

1. 定义自定义 BringIntoViewSpec

借助以下辅助可组合项,您可以根据父级和子级比例定义“透视”。parentFraction 决定了项应落在容器中的哪个位置,而 childFraction 决定了项的哪个部分与该点对齐。

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PositionFocusedItemInLazyLayout(
    parentFraction: Float = 0.3f,
    childFraction: Float = 0f,
    content: @Composable () -> Unit,
) {
    val bringIntoViewSpec = remember(parentFraction, childFraction) {
        object : BringIntoViewSpec {
            override fun calculateScrollDistance(
                offset: Float,       // Item's initial position
                size: Float,         // Item's size
                containerSize: Float // Container's size
            ): Float {
                // Calculate the offset position of the item's leading edge.
                val initialTargetForLeadingEdge =
                    parentFraction * containerSize - (childFraction * size)
                // If the item fits in the container, and scrolling would cause
                // its trailing edge to be clipped, adjust targetForLeadingEdge
                // to prevent over-scrolling near the end of list.
                val targetForLeadingEdge = if (size <= containerSize &&
                    (containerSize - initialTargetForLeadingEdge) < size) {
                    // If clipped, align the item's trailing edge with the
                    // container's trailing edge.
                    containerSize - size
                } else {
                    initialTargetForLeadingEdge
                }
                // Return scroll distance relative to initial item position.
                return offset - targetForLeadingEdge
            }
        }
    }

    // Apply the spec to all scrollables in the hierarchy
    CompositionLocalProvider(
        LocalBringIntoViewSpec provides bringIntoViewSpec,
        content = content,
    )
}

2. 应用自定义规范

使用辅助函数封装布局,以应用定位。这对于在目录的不同行中创建“一致的焦点线”非常有用。

PositionFocusedItemInLazyLayout(
    parentFraction = 0.3f, // Pivot 30% from the edge
    childFraction = 0.5f   // Center of the item aligns with the pivot
) {
    LazyColumn {
        items(sectionList) { section ->
            // This row and its items will respect the 30% pivot
            LazyRow { /* ... */ }
        }
    }
}

3. 为特定嵌套布局选择退出

如果您有特定的嵌套布局,应使用标准滚动行为而不是自定义轴心,请提供 DefaultBringIntoViewSpec:

private val DefaultBringIntoViewSpec = object : BringIntoViewSpec {}

PositionFocusedItemInLazyLayout {
    LazyColumn {
        item {
            // This row will ignore the custom pivot and use default behavior
            CompositionLocalProvider(LocalBringIntoViewSpec provides DefaultBringIntoViewSpec) {
                LazyRow { /* ... */ }
            }
        }
    }
}

实际上,通过传递空 BringIntoViewSpec,可以启用框架的默认行为。

从 TV Foundation 迁移到 Compose Foundation

androidx.tv.foundation 中 TV 专用的延迟布局已弃用,取而代之的是标准 Compose Foundation 布局。

依赖项更新

验证您的 build.gradle 是否使用 1.7.0 或更高版本:

  • androidx.compose.foundation
  • androidx.compose.runtime

组件映射

如需迁移,请更新导入内容,并从组件中移除 Tv 前缀:

已废弃的 TV 组件 Compose Foundation 替代方案
TvLazyRow LazyRow
TvLazyColumn LazyColumn
TvLazyHorizontalGrid LazyHorizontalGrid
TvLazyVerticalGrid LazyVerticalGrid
pivotOffsets BringIntoViewSpec(通过 LocalBringIntoViewSpec)