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。
  • 具备有关可组合项和修饰符的基础知识。

构建内容

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

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

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

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

所需条件

2. 进行设置

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

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

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

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

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

3. 实现目录浏览器页面

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

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

CatalogBrowser.kt

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.material3.ExperimentalTvMaterial3Api
import com.example.tvcomposeintroduction.data.Movie

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

显示类别名称

您可以使用 catalogBrowserViewModel.categoryList 属性访问类别列表,该属性是 Category 列表的流程。该流程通过调用 Compose State 对象来收集,方法是调用其 collectAsState 方法。Category 对象具有 name 属性,该属性是表示类别名称的 String 值。

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

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

CatalogBrowser.kt

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie

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

显示每个类别的内容列表

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

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

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

CatalogBrowser.kt

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

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

可选:调整布局

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

CatalogBrowser.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = viewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsState()
    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 Composable 函数。您需要为此函数添加代码,以实现详情页面。

Details.kt

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.tv.material3.ExperimentalTvMaterial3Api
import com.example.tvcomposeintroduction.data.Movie

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

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

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

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

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

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

Details.kt

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie

@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.title)
    }
}

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

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

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

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

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

Details.kt

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import com.example.tvcomposeintroduction.data.Movie

@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
                .padding(vertical = 32.dp, horizontal = 48.dp)
        ) {
            Text(
                text = movie.title,
            )
            Text(
                text = movie.studio,
            )
            Text(
                text = movie.title,
            )
        }
    }
}

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

MaterialTheme 对象包含引用当前主题值的函数,例如 Typography 和 [ColorScheme][ColorScheme] 类中的函数。

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

  1. MaterialTheme.typography.headlineLarge 属性设置为影片名的文本样式。
  2. MaterialTheme.typography.headlineMedium 属性设置为其他两个 Text Composable 函数的文本样式。
  3. 使用 Modifier.background 方法将 MaterialTheme.colorScheme.background 属性设置为 Column Composable 函数的背景颜色。

[ColorScheme]: /reference/kotlin/androidx/tv/material3/ColorScheme)

Details.kt

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import com.example.tvcomposeintroduction.data.Movie

@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
                .padding(vertical = 32.dp, horizontal = 48.dp)
        ) {
            Text(
                text = movie.title,
                style = MaterialTheme.typography.headlineLarge,
            )
            Text(
                text = movie.studio,
                style = MaterialTheme.typography.headlineMedium,
            )
            Text(
                text = movie.title,
                style = MaterialTheme.typography.headlineMedium,
            )
        }
    }
}

5. 添加界面之间的导航

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

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

CatalogBrowser.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = viewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsState()
    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 Composable 函数添加项。
  3. 在传递给 item 函数的 lambda 中将 featuredMovieList 声明为委托属性,然后将 State 对象设置为委托,后者是从 catalogBrowserViewModel.featuredMovieList 属性收集的。
  4. item 函数中调用 Carousel Composable 函数,然后传入以下参数:
  • 通过 slideCount 参数设置的 featuredMovieList 变量的大小。
  • 使用 Modifier.fillMaxWidthModifier.height 方法指定轮播界面大小的 Modifier 对象。Carousel Composable 函数通过将 376.dp 值传递给 Modifier.height 方法来设置 376 dp 的高度。
  • 通过整数值调用的 lambda 表示可见轮播项的索引。
  1. 通过 featuredMovieList 变量和指定的索引值中检索 Movie 对象。
  2. CarouselSlide Composable 函数添加到 Carousel Composable 函数。
  3. CarouselSlide Composable 函数添加 Text Composable 函数,以显示影片名。

CatalogBrowser.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Carousel
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = viewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsState()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsState()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp)
            ) { indexOfCarouselSlide ->
                val featuredMovie =
                    featuredMovieList[indexOfCarouselSlide]
                CarouselSlide {
                    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) })
                }
            }
        }
    }
}

显示背景图片

CarouselSlide Composable 函数可以接受另一个 lambda,以指定 CarouselSlide Composable 函数的背景的显示方式。

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

  1. 将 lambda 传递给带有 background 参数的 CarouselSlide Composable 函数。
  2. 调用 AsyncImage Composable 函数,将与 Movie 对象相关联的背景图片加载到 CarouselSlide Composable 函数的背景。
  3. 更新 CarouselSlide Composable 函数中 Text Composable 函数的位置和文本样式,以提高曝光度。
  4. AsyncImage Composable 函数设置占位符以避免布局偏移。起始代码有一个占位符作为可绘制对象,您可以使用 R.drawable.placeholder 引用该可绘制对象。

CatalogBrowser.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Carousel
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import com.example.tvcomposeintroduction.R
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = viewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsState()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsState()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp),
            ) { indexOfCarouselItem ->
                val featuredMovie = featuredMovieList[indexOfCarouselItem]
                CarouselSlide(
                    background = {
                        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) })
                }
            }
        }
    }
}

向详情页面添加屏幕转换

您可以让用户点击 CarouselSlide Composable 函数。

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

  1. 通过 modifier 参数将 Modifier.clickable 方法的返回值传递给 CarouselSlide Composable 函数。
  2. 在传递给 Modifier.clickable 方法的 lambda 中,使用可见的 CarouselSlide Composable 函数的 Movie 对象调用 onMovieSelected 函数。

CatalogBrowser.kt

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Carousel
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import com.example.tvcomposeintroduction.R
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = viewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsState()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsState()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp),
            ) { indexOfCarouselItem ->
                val featuredMovie = featuredMovieList[indexOfCarouselItem]
                CarouselSlide(
                    background = {
                        AsyncImage(
                            model = featuredMovie.backgroundImageUrl,
                            contentDescription = null,
                            placeholder = painterResource(
                                id = R.drawable.placeholder
                            ),
                            contentScale = ContentScale.Crop,
                            modifier = Modifier.fillMaxSize(),
                        )
                    },
                    modifier = Modifier.clickable { onMovieSelected(featuredMovie) }
                ) {
                    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) })
                }
            }
        }
    }
}

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 实现一个显示内容列表的页面。
  • 用于显示内容详情的基本页面实现。
  • 如何在两个页面之间添加页面转换。