Compose for TV 简介

1. 开始之前

Compose for TV 是用于开发在 Android TV 上运行的应用的最新 UI 框架。它为 TV 应用解锁了 Jetpack Compose 的所有优势,让您的应用更轻松地构建美观且功能强大的 UI。Compose for TV 的一些特定优势包括:

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

电视应用的一个常见用例是媒体消费。用户浏览内容目录并选择他们想要观看的内容。内容可以是电影、电视节目或播客。用户选择内容后,他们可能希望查看更多相关信息,例如简短描述、播放时长和创作者姓名。在本 Codelab 中,您将学习如何使用 Compose for TV 实现目录浏览器屏幕和详细信息屏幕。

前提条件

  • 熟悉 Kotlin 语法,包括 lambda 表达式。
  • 具有 Compose 的基本经验。如果您不熟悉 Compose,请完成Jetpack Compose 基础知识 Codelab。
  • 具备可组合项和修饰符的基本知识。
  • 以下任一设备,用于运行示例应用:
    • Android TV 设备
    • 具有 TV 设备定义类别中配置文件的 Android 虚拟设备

您将构建什么

  • 一个包含目录浏览器屏幕和详细信息屏幕的视频播放器应用。
  • 一个目录浏览器屏幕,显示供用户选择的视频列表。它看起来像下图:

The catalog browser displays a list of featured movies\nwith a carousel on top.\nThe screen also displays a list of movies for each category.

  • 一个详细信息屏幕,显示所选视频的元数据,如标题、描述和时长。它看起来像下图:

The details screen displays the movie's metadata,\nincluding its title, studio, and short description.\nThe metadata is displayed on the background image associated with the movie.

您需要什么

  • 最新版本的Android Studio
  • Android TV 设备或 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 列表的。通过调用其 collectAsStateWithLifecycle 方法,该流被收集为 Compose State 对象。Category 对象具有 name 属性,该属性是一个表示类别名称的 String 值。

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

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

CatalogBrowser.kt

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

显示每个类别的内容列表

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

要显示每个类别的内容列表,请按照以下步骤操作:

  1. 添加 LazyRow 可组合函数,然后向其传递一个 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()
    LazyColumn(modifier = modifier) {
        items(categoryList) { category ->
            Text(text = category.name)
            LazyRow {
                items(category.movieList) {movie ->
                    MovieCard(movie = movie)
                }
            }
        }
    }
}

可选:调整布局

  1. 要设置类别之间的间距,请将 Arrangement 对象以及 verticalArrangement 参数传递给 LazyColumn 可组合函数。通过调用 Arrangement#spacedBy 方法来创建 Arrangement 对象。
  2. 要设置电影卡之间的间距,请将 Arrangement 对象以及 horizontalArrangement 参数传递给 LazyRow 可组合函数。
  3. 要设置列的缩进,请使用 contentPadding 参数传递 PaddingValue 对象。

CatalogBrowser.kt

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

4. 实现详情屏幕

详细信息屏幕显示所选电影的详细信息。Details.kt 文件中有一个 Details 可组合函数。您将在此函数中添加代码以实现详细信息屏幕。

Details.kt

@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

@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. Box 可组合函数添加一个 AsyncImage 可组合函数,并使用以下参数:
  • 将给定 Movie 对象的 backgroundImageUrl 属性值设置为 model 参数。
  • contentDescription 参数传递 null
  • contentScale 参数传递 ContentScale.Crop 对象。要查看不同的 ContentScale 选项,请参阅内容比例
  • Modifier.fillMaxSize 方法的返回值传递给 modifier 参数。

Details.kt

@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

@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. 使用通过调用 Modifier.width 函数并传递 0.5f 值来创建的 width 修饰符设置 Column 可组合函数的宽度
  5. 在第二个 Text 可组合函数和第三个 Text 可组合函数之间添加 8.dp 的间距,使用 SpacerSpacer 可组合函数的高度通过使用 Modifier.height 函数创建的 height 修饰符指定

Details.kt

@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. 将 lambda 函数与 onClick 参数一起传递给 MovieCard 可组合函数。
  3. 使用与 MovieCard 可组合函数关联的电影对象调用 onMovieSelected 回调。

CatalogBrowser.kt

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

6. 在目录浏览器屏幕中添加轮播,以突出显示特色内容

轮播是一种常用的 UI 组件,它会在指定持续时间后自动更新幻灯片。它通常用于突出显示特色内容。

要向目录浏览器屏幕添加轮播以突出显示特色内容列表中的电影,请按照以下步骤操作:

  1. 打开 com.example.tvcomposeintroduction.ui.screens.CatalogBrowser 文件。
  2. 调用 item 函数,向 LazyColumn 可组合函数添加一个项。
  3. 在传递给 item 函数的 lambda 表达式中,将 featuredMovieList 声明为委托属性,然后设置要委托的 State 对象,该对象是从 catalogBrowserViewModel.featuredMovieList 属性收集的。
  4. item 函数内部调用 Carousel 可组合函数,然后传入以下参数:
  • 通过 slideCount 参数传递 featuredMovieList 变量的大小。
  • 一个 Modifier 对象,用于使用 Modifier.fillMaxWidthModifier.height 方法指定轮播大小。Carousel 可组合函数通过向 Modifier.height 方法传递 376.dp 值来使用 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()
    LazyColumn(
        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)
            LazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

显示背景图片

Box 可组合函数将一个组件放置在另一个组件之上。有关详细信息,请参阅布局基础知识

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

  1. Text 可组合函数之前,调用 AsyncImage 可组合函数以加载与 Movie 对象关联的背景图片。
  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()
    LazyColumn(
        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)
            LazyRow(
                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 可组合函数中,使用调用 stringResource 函数并传入 R.string.show_details 获得的返回值来调用 Text 可组合函数。
  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()
    LazyColumn(
        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)
            LazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

可选:调整布局

要调整轮播图的布局,请遵循以下步骤

  1. Carousel 可组合函数中,将 backgroundColor 值赋为 MaterialTheme.colorScheme.background
  2. Box 可组合项包装 Column 可组合函数
  3. Alignment.BottomStart 值传递给 Box 组件的 contentAlignment 参数。
  4. fillMaxSize 修饰符传递给 Box 可组合函数的 modifier 参数。fillMaxSize 修饰符是使用 Modifier.fillMaxSize() 函数创建的。
  5. 在传递给 Box 可组合函数的 fillMaxSize 修饰符之上调用 drawBehind() 方法
  6. 在传递给 drawBehind 修饰符的 lambda 表达式中,将 brush 值赋给通过调用 Brush.linearGradient 函数并传递两个 Color 对象的列表来创建的 Brush 对象。该列表是通过调用 listOf 函数并传递 backgroundColor 值和 Color.Transparent 值来创建的。
  7. 在传递给 drawBehind 修饰符的 lambda 表达式中调用 drawRect 并传入 brush 对象,以便在背景图像上方创建一个蒙版层
  8. 使用通过调用 Modifier.padding 并传入 20.dp 值来创建的 padding 修饰符指定 Column 可组合函数的内边距。
  9. Column 可组合函数中,在 Text 可组合项和 Button 可组合项之间添加一个值为 20.dpSpacer 可组合函数

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    LazyColumn(
        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)
            LazyRow(
                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 的基础知识:

  • 如何通过结合 LazyColumn 和 LazyRow 实现显示内容列表的屏幕。
  • 显示内容详细信息的基本屏幕实现。
  • 如何在两个屏幕之间添加屏幕转换。