Compose for TV 简介

1. 准备工作

Compose for TV 是用于开发在 Android TV 上运行的应用的最新应用框架。它融合了适用于 TV 应用的 Jetpack Compose 的所有优势,可让您更轻松地为应用构建功能强大且美观出众的界面。Compose for TV 的一些具体优势包括:

  • 灵活性。Compose 可用于创建任何类型的界面,从简单的布局到复杂的动画。组件是可直接使用的,但也可以进行自定义和样式设置,以满足您的应用的需求。
  • 简化并加速开发。Compose 与现有代码兼容,让开发者可以使用更少的代码构建应用。
  • 直观:Compose 使用声明式语法,直观地更改界面以及调试、理解和审核代码。

TV 应用的常见应用场景是媒体使用。用户浏览内容目录并选择想要观看的内容。这些内容可以是电影、电视节目或播客。用户选择一段内容后,他们可能需要查看更多信息,例如简短说明、播放时长和创作者的姓名。在本 Codelab 中,您将学习如何使用 Compose for TV 实现目录浏览器页面和详情页面。

前提条件

  • 有使用 Kotlin 语法(包括 lambda)的经验。
  • 有使用 Compose 的基本经验。如果您不熟悉 Compose,请先完成 Jetpack Compose 基础知识 Codelab。
  • 具备可组合函数和修饰符方面的基础知识。
  • 用于运行示例应用的以下任意设备:
    • Android TV 设备
    • Android 虚拟设备,包含属于 TV 设备定义类别的配置文件。

构建内容

  • 您将构建一个视频播放器应用,其中包含一个目录浏览器页面和一个详情页面。
  • 目录浏览器页面将显示一系列视频供用户选择,如下图所示:

目录浏览器显示了一个精选电影列表\n顶部有一个轮播界面。\n页面上还显示了各个类别的电影列表。

  • 详情页面会显示所选视频的元数据,例如标题、说明和时长,如下图所示:

详情页面上显示了电影的元数据,\n包括影片名、工作室和简短说明。\n元数据显示在与电影相关联的背景图片中。

所需条件

  • 最新版本的 Android Studio
  • Android TV 设备或属于电视设备类别的虚拟设备

2. 进行设置

如需获取包含此 Codelab 的主题和基本设置的代码,请执行以下任一操作:

$ git clone https://github.com/android/tv-codelabs.git

main 分支包含起始代码,solution 分支包含解决方案代码。

  • 下载 main.zip 文件(包含起始代码)和 solution.zip 文件(包含解决方案代码)。

现在,您已下载相应代码,请在 Android Studio 中打开 IntroductionToComposeForTV 项目文件夹。现已准备就绪,可以开始开发项目了。

3. 实现目录浏览器页面

用户可以在目录浏览器页面上浏览电影目录。您将目录浏览器实现为可组合函数。您可以在 CatalogBrowser.kt 文件中找到 CatalogBrowser 可组合函数。您将在此可组合函数中实现目录浏览器页面。

起始代码有一个名为 CatalogBrowserViewModel 类的 ViewModel,其中包含多个属性和方法,可用于检索描述电影内容的 Movie 对象。您需要使用检索到的 Movie 对象来实现一个目录浏览器。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
}

显示类别名称

您可以使用 catalogBrowserViewModel.categoryList 属性访问类别列表,该属性是 Category 列表的数据流。该数据流是以 ComposeState 对象的形式收集的(通过调用其 collectAsStateWithLifecycle 方法)。Category 对象具有 name 属性,该属性是表示类别名称的 String 值。

如需显示类别名称,请按以下步骤操作:

  1. 在 Android Studio 中,打开起始代码的 CatalogBrowser.kt 文件,然后将 TvLazyColumn 可组合函数添加到 CatalogBrowser 可组合函数。
  2. 调用 catalogBrowserViewModel.categoryList.collectAsStateWithLifeCycle() 方法以 State 对象的形式收集数据流。
  3. categoryList 声明为您在上一步中创建的 State 对象的委托属性。
  4. 使用 categoryList 变量作为参数来调用 items 函数。
  5. 使用类别名称作为参数调用 Text 可组合函数,该参数将作为 lambda 的参数传递。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    TvLazyColumn(modifier = modifier) {
        items(categoryList) { category ->
            Text(text = category.name)
        }
    }
}

显示每个类别的内容列表

Category 对象有另一个名为 movieList 的属性。该属性是一系列 Movie 对象,表示属于此类别的电影。

如需显示各个类别的内容列表,请按以下步骤操作:

  1. 添加 TvLazyRow 可组合函数,然后向其传递一个 lambda。
  2. 在 lambda 中,使用 category.movieList 属性值调用 items 函数,然后向其传递一个 lambda。
  3. 在传递给 items 函数的 lambda 中,使用 Movie 对象调用 MovieCard 可组合函数。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    TvLazyColumn(modifier = modifier) {
        items(categoryList) { category ->
            Text(text = category.name)
            TvLazyRow {
                items(category.movieList) {movie ->
                    MovieCard(movie = movie)
                }
            }
        }
    }
}

可选:调整布局

  1. 如需设置类别之间的间隔,请将 Arrangement 对象作为 verticalArrangement 参数传递给 TvLazyColumn 可组合函数。可通过调用 Arrangement#spacedBy 方法来创建 Arrangement 对象。
  2. 如需设置电影卡片之间的间隔,请将 Arrangement 对象作为 horizontalArrangement 参数传递给 TvLazyRow 可组合函数。
  3. 如需为列设置缩进,请将 PaddingValue 对象作为 contentPadding 参数传递。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifeCycle()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        items(categoryList) { category ->
            Text(text = category.name)
            TvLazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie)
                }
            }
        }
    }
}

4. 实现详情页面

详情页面会显示所选影片的详细信息。Details.kt 文件中有一个 Details 可组合函数。您需要为此函数添加代码,以实现详情页面。

Details.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
}

显示影片名、工作室名称和说明

Movie 对象有以下三个字符串属性作为电影的元数据:

  • title:影片名。
  • studio:制作该电影的工作室名称。
  • description:影片的简短摘要。

如需在详情页面上显示此元数据,请按以下步骤操作:

  1. 添加一个 Column 可组合函数,然后使用通过 Modifier.padding 方法创建的 Modifier 对象,将列的垂直间距设置为 32 dp,水平间距设置为 48 dp。
  2. 添加一个用于显示影片名的 Text 可组合函数。
  3. 添加一个用于显示工作室名称的 Text 可组合函数。
  4. 添加一个用于显示影片说明的 Text 可组合函数。

Details.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
    Column(
        modifier = Modifier
            .padding(vertical = 32.dp, horizontal = 48.dp)
    ) {
        Text(text = movie.title)
        Text(text = movie.studio)
        Text(text = movie.description)
    }
}

Details 可组合函数的参数中指定的 Modifier 对象会在下一个任务中使用。

显示与指定的 Movie 对象关联的背景图片

Movie 对象具有 backgroundImageUrl 属性,该属性指示了该对象描述的电影的背景图片位置。

如需显示指定影片的背景图片,请按以下步骤操作:

  1. 添加一个 Box 可组合函数作为 Column 可组合函数的封装容器,其中包含通过 Details 可组合函数传递的 modifier 对象。
  2. Box 可组合函数中,调用 modifier 对象的 fillMaxSize 方法,使 Box 可组合函数填充能够分配给 Details 可组合函数的最大大小。
  3. 将包含以下参数的 AsyncImage 可组合函数添加到 Box 可组合函数中:
  • 将给定 Movie 对象的 backgroundImageUrl 属性的值设置为 model 参数。
  • null 传递给 contentDescription 参数。
  • ContentScale.Crop 对象传递给 contentScale 参数。若想查看不同的 ContentScale 选项,请参阅内容缩放
  • Modifier.fillMaxSize 方法的返回值传递给 modifier 参数。

Details.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize()) {
        AsyncImage(
            model = movie.cardImageUrl,
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )
        Column {
            Text(
                text = movie.title,
            )
            Text(
                text = movie.studio,
            )
            Text(text = movie.description)
        }
    }
}

引用 MaterialTheme 对象以确保一致的主题设置

MaterialTheme 对象包含用于引用当前主题值的函数,例如 TypographyColorScheme 类中的函数。

如需引用 MaterialTheme 对象以确保一致的主题设置,请按以下步骤操作:

  1. MaterialTheme.typography.displayMedium 属性设置为影片名的文本样式。
  2. MaterialTheme.typography.bodySmall 属性设置为第二个 Text 可组合函数的文本样式。
  3. 使用 Modifier.background 方法将 MaterialTheme.colorScheme.background 属性设置为 Column 可组合函数的背景颜色。

Details.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize()) {
        AsyncImage(
            model = movie.cardImageUrl,
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )
        Column(
            modifier = Modifier
                .background(MaterialTheme.colorScheme.background),
        ) {
            Text(
                text = movie.title,
                style = MaterialTheme.typography.displayMedium,
            )
            Text(
                text = movie.studio,
                style = MaterialTheme.typography.bodySmall,
            )
            Text(text = movie.description)
        }
    }
}

可选:调整布局

如需调整 Details 可组合函数的布局,请按以下步骤操作:

  1. 设置 Box 可组合函数,通过 fillMaxSize 修饰符使用整个可用空间
  2. 使用 background 修饰符设置 Box 可组合函数的背景,以便使用通过调用 Brush.linearGradient 函数(使用一系列包含 MaterialTheme.colorScheme.background 值和 Color.TransparentColor 对象)创建的线性渐变来填充背景。
  3. 使用 padding 修饰符将 Column 可组合函数周围的水平间距设置为 48.dp,垂直间距设置为 24.dp
  4. 使用 width 修饰符设置 Column 可组合函数的宽度,该修饰符是通过调用 Modifier.width 函数(使用 0.5f 值)创建的
  5. 使用 Spacer 在第二个 Text 可组合函数和第三个 Text 可组合项之间添加 8.dp 的空间。使用通过 Modifier.height 函数创建的 height 修饰符指定 Spacer 可组合函数的高度

Details.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize()) {
        AsyncImage(
            model = movie.cardImageUrl,
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )
        Box(
            modifier = Modifier
                .background(
                    Brush.linearGradient(
                        listOf(
                            MaterialTheme.colorScheme.background,
                            Color.Transparent
                        )
                    )
                )
                .fillMaxSize()
        ) {
            Column(
                modifier = Modifier
                    .padding(horizontal = 48.dp, vertical = 24.dp)
                    .fillMaxWidth(0.5f)
            ) {
                Text(
                    text = movie.title,
                    style = MaterialTheme.typography.displayMedium,
                )
                Text(
                    text = movie.studio,
                    style = MaterialTheme.typography.bodySmall,
                )
                Spacer(modifier = Modifier.height(8.dp))
                Text(
                    text = movie.description,
                )
            }
        }
    }
}

5. 添加界面之间的导航

现在,您已拥有目录浏览器页面和详情页面。用户在目录浏览器页面上选择内容后,相应页面必须转换为详情页面。为此,您可以使用 clickable 修饰符为 MovieCard 可组合函数添加 event 监听器。当用户按方向键的中心按钮时,将调用 CatalogBrowserViewModel#showDetails 方法(使用与 MovieCard 可组合函数关联的影片对象作为参数)。

  1. 打开 com.example.tvcomposeintroduction.ui.screens.CatalogBrowser 文件。
  2. 使用 onClick 参数将 lambda 函数传递给 MovieCard 可组合函数。
  3. 使用与 MovieCard 可组合函数关联的影片对象调用 onMovieSelected 回调。

CatalogBrowser.kt

@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        items(categoryList) { category ->
            Text(text = category.name)
            TvLazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

6. 在目录浏览器页面中添加轮播界面,以突出显示精选内容

轮播界面是一个自适应调整的界面组件,会在指定时长后自动更新其幻灯片,通常用于突出显示精选内容。

如需将轮播界面添加到目录浏览器页面以突出显示精选内容列表中的电影,请按以下步骤操作:

  1. 打开 com.example.tvcomposeintroduction.ui.screens.CatalogBrowser 文件。
  2. 调用 item 函数,为 TvLazyColumn 可组合函数添加项。
  3. 在传递给 item 函数的 lambda 中将 featuredMovieList 声明为委托属性,然后将 State 对象设置为委托对象,后者是从 catalogBrowserViewModel.featuredMovieList 属性收集的。
  4. item 函数中调用 Carousel 可组合函数,然后传入以下参数:
  • 通过 slideCount 参数设置的 featuredMovieList 变量的大小。
  • 使用 Modifier.fillMaxWidthModifier.height 方法指定轮播界面大小的 Modifier 对象。Carousel 可组合函数通过将 376.dp 值传递给 Modifier.height 方法,设置 376 dp 的高度。
  • 通过整数值调用的 lambda,用于指明可见轮播项的索引。
  1. 通过 featuredMovieList 变量和指定的索引值检索 Movie 对象。
  2. Carousel 可组合函数添加 Box 可组合函数。
  3. Box 可组合函数添加 Text 可组合函数,以显示影片名。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsStateWithLifecycle()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp)
            ) { indexOfCarouselSlide ->
                val featuredMovie =
                    featuredMovieList[indexOfCarouselSlide]
                Box {
                    Text(text = featuredMovie.title)
                }
            }
        }
        items(categoryList) { category ->
            Text(text = category.name)
            TvLazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

显示背景图片

Box 可组合函数会将一个组件放在另一个组件上。如需了解详情,请参阅布局基础知识

如需显示背景图片,请按以下步骤操作:

  1. 先调用 AsyncImage 可组合函数,加载与 Movie 对象关联的背景图片,然后再调用 Text 可组合函数。
  2. 更新 Text 可组合函数的位置和文本样式,以提高曝光度。
  3. AsyncImage 可组合函数设置占位符以避免布局偏移。起始代码有一个占位符作为可绘制对象,您可以使用 R.drawable.placeholder 引用该可绘制对象。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsStateWithLifecycle()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp),
            ) { indexOfCarouselItem ->
                val featuredMovie = featuredMovieList[indexOfCarouselItem]
                Box{
                    AsyncImage(
                        model = featuredMovie.backgroundImageUrl,
                        contentDescription = null,
                        placeholder = painterResource(
                            id = R.drawable.placeholder
                        ),
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.fillMaxSize(),
                    )
                    Text(text = featuredMovie.title)
                }
            }
        }
        items(categoryList) { category ->
            Text(text = category.name)
            TvLazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

添加切换到详情页面的页面转换

您可以向轮播界面添加 Button,以便用户可以通过点击相应按钮,触发切换到详情页面的页面转换。

若想让用户可在详情页面的可见轮播界面中查看影片详情,请按以下步骤操作:

  1. Carousel 可组合函数的 Box 可组合函数中调用 Column 可组合函数
  2. Carousel 中的 Text 可组合函数移至 Column 可组合函数
  3. Column 可组合函数中的 Text 可组合函数后,调用 Button 可组合函数
  4. Button 可组合函数中调用 Text 可组合函数,并使用 R.string.show_details 调用的 stringResource 函数的返回值。
  5. 在传递给 Button 可组合函数的 onClick 参数的 lambda 中,使用 featuredMovie 变量调用 onMovieSelected 函数

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsStateWithLifecycle()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp),
            ) { indexOfCarouselItem ->
                val featuredMovie = featuredMovieList[indexOfCarouselItem]
                Box {
                    AsyncImage(
                        model = featuredMovie.backgroundImageUrl,
                        contentDescription = null,
                        placeholder = painterResource(
                            id = R.drawable.placeholder
                        ),
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.fillMaxSize(),
                    )
                    Column {
                        Text(text = featuredMovie.title)
                        Button(onClick = { onMovieSelected(featuredMovie) }) {
                            Text(text = stringResource(id = R.string.show_details))
                        }
                    }
                }
            }
        }
        items(categoryList) { category ->
            Text(text = category.name)
            TvLazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

可选:调整布局

如需调整轮播界面的布局,请按以下步骤操作:

  1. Carousel 可组合函数中使用 MaterialTheme.colorScheme.background 值分配 backgroundColor
  2. 使用 Box 可组合函数封装 Column 可组合函数
  3. Alignment.BottomStart 值传递给 Box 组件的 contentAlignment 参数。
  4. fillMaxSize 修饰符传递给 Box 可组合函数的修饰符参数。fillMaxSize 修饰符是使用 Modifier.fillMaxSize() 函数创建的。
  5. 对传递给 Box 可组合函数的 fillMaxSize 修饰符调用 drawBehind() 方法
  6. 在传递给 drawBehind 修饰符的 lambda 中,使用 Brush 对象分配 brush 值,该对象是通过调用 Brush.linearGradient 函数(使用包含两个 Color 对象的列表)创建的。上述列表是通过调用 listOf 函数(使用 backgroundColor 值和 Color.Transparent 值)创建的。
  7. 使用传递给 drawBehind 修饰符的 lambda 中的 brush 对象调用 drawRect,在背景图片上制作 srim 层
  8. 使用 padding 修饰符指定 Column 可组合函数的内边距,该修饰符是通过调用 Modifier.padding(使用 20.dp 值)创建的。
  9. Column 可组合函数中的 Text 可组合函数和 Button 可组合函数之间添加一个包含 20.dp 值的 Spacer 可组合函数

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(32.dp),
        contentPadding = PaddingValues(horizontal = 58.dp, vertical = 36.dp)
    ) {
        item {
            val featuredMovieList by
            catalogBrowserViewModel.featuredMovieList.collectAsStateWithLifecycle()

            Carousel(
                itemCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp),
            ) { indexOfCarouselItem ->
                val featuredMovie = featuredMovieList[indexOfCarouselItem]
                val backgroundColor = MaterialTheme.colorScheme.background

                Box {
                    AsyncImage(
                        model = featuredMovie.backgroundImageUrl,
                        contentDescription = null,
                        placeholder = painterResource(
                            id = R.drawable.placeholder
                        ),
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.fillMaxSize(),
                    )
                    Box(
                        contentAlignment = Alignment.BottomStart,
                        modifier = Modifier
                            .fillMaxSize()
                            .drawBehind {
                                val brush = Brush.horizontalGradient(
                                    listOf(backgroundColor, Color.Transparent)
                                )
                                drawRect(brush)
                            }
                    ) {
                        Column(
                            modifier = Modifier.padding(20.dp)
                        ) {
                            Text(
                                text = featuredMovie.title,
                                style = MaterialTheme.typography.displaySmall
                            )
                            Spacer(modifier = Modifier.height(28.dp))
                            Button(onClick = { onMovieSelected(featuredMovie) }) {
                                Text(text = stringResource(id = R.string.show_details))
                            }
                        }
                    }
                }
            }
        }
        items(categoryList) { category ->
            Text(text = category.name)
            TvLazyRow(
                horizontalArrangement = Arrangement.spacedBy(16.dp),
                modifier = Modifier.height(200.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(
                        movie,
                        onClick = {
                            onMovieSelected(it)
                        }
                    )
                }
            }
        }
    }
}

7. 获取解决方案代码

如需下载此 Codelab 的解决方案代码,请执行以下操作之一:

  • 点击下面的按钮,将项目下载为 ZIP 文件,然后进行解压缩并在 Android Studio 中将其打开。

  • 使用 Git 检索该文件:
$ git clone https://github.com/android/tv-codelabs.git
$ cd tv-codelabs
$ git checkout solution
$ cd IntroductionToComposeForTV

8. 恭喜。

恭喜!您已经学完了 Compose for TV 的基础知识:

  • 如何通过组合 TvLazyColumn 和 TvLazyLow 实现一个显示内容列表的页面。
  • 用于显示内容详情的基本页面实现。
  • 如何在两个页面之间添加页面转换。