Jetpack Compose tvのCarouselを使う

# 概要
Jetpack Compose tvにあるCarouselを使うことで、以下のような、自動で画面が切り替わるUIが実現できます。
![Jetpack Compose tvでCarouselを使ったUI](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/a474fb2c-7e3b-e852-cffb-942284f86bde.gif)

# Jetpack Compose tvについて
Jetpack Compose tvは2022/10/05にリリースされた、テレビ用のアプリを作成するためのComposeになります。
今回紹介するCarousel以外の、`TvLazyRow` や `ImmersiveList`などのAPIに関しては[以前投稿した記事](https://qiita.com/hyuga_amazia/items/492ea43c4cf7ee225332)がありますので、そちらを参照してください。

# 下準備
[tv](https://developer.android.com/jetpack/androidx/releases/tv?hl=ja)のドキュメントに沿って依存関係の宣言をしていきます。

```build.gradle
dependencies {
    implementation "androidx.tv:tv-foundation:1.0.0-alpha03"
    implementation "androidx.tv:tv-material:1.0.0-alpha03"
}
```

# Carouselを使う

Carouselの最短のコードは以下になります。

```kotlin
    Carousel(slideCount = 10) {
        // indexによって画像切り替え
        val data = when (it % 2) {
            0 -> R.drawable.app_icon_your_company
            else -> R.drawable.movie
        }
        Image(painter = painterResource(id = data),
            contentDescription = null,
            contentScale = ContentScale.FillWidth,
            modifier = Modifier.fillMaxSize())
    }
```
全体のスライドの数の`slideCount`と、indexによって表示するcontentを指定することによって先ほどのようなCarouselが実現できます。

## 他の要素

### carouselState
`CarouselState` を指定することができます。CarouselStateは`initialSlideIndex`を持つので、インデックスを切り替えたりする用途に使えます。

### timeToDisplaySlideMillis
スライドが切り替わるのに必要なミリ秒数です。デフォルトは5000ミリ秒です。
指定するとスライドの切り替わる秒数を変更できます。

### enterTransition, exitTransition
スライドが切り替わる際の画面効果を切り替えることができます。

### carouselIndicator
スライドの位置や全体数を示すインジケーターを設定できます。Boxを用いて、柔軟なインジケータを作成することが可能です。

## フルに生かしてみる

```kotlin
@OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
@Composable
fun CarouselView(slideCount: Int, initialSlideIndex: Int) {
    val carouselState = rememberCarouselState(initialSlideIndex)
    Carousel(slideCount = slideCount,
        carouselState = carouselState,
        timeToDisplaySlideMillis = 1000,
        enterTransition = scaleIn(),
        exitTransition = slideOutVertically(),
        carouselIndicator = {
            val defaultSize = remember { 8.dp }
            val inactiveColor = remember { Color.LightGray }
            val activeColor = remember { Color.White }
            Box {
                Row(
                    horizontalArrangement = Arrangement.spacedBy(defaultSize),
                    verticalAlignment = Alignment.CenterVertically,
                ) {
                    repeat(slideCount) {
                        Text(when(it) {
                            0 -> "い"
                            1 -> "ろ"
                            2 -> "は"
                            3 -> "に"
                            4 -> "ほ"
                            5 -> "へ"
                            6 -> "と"
                            7 -> "ち"
                            8 -> "り"
                            9 -> "ぬ"
                            else -> "ぺ"
                        }, color =
                        if (it == carouselState.slideIndex) {
                            activeColor
                        } else {
                            inactiveColor
                        })
                    }
                }
            }
    }) {
        // indexによって画像切り替え
        val data = when (it % 2) {
            0 -> R.drawable.app_icon_your_company
            else -> R.drawable.movie
        }
        Image(painter = painterResource(id = data),
            contentDescription = null,
            contentScale = ContentScale.FillWidth,
            modifier = Modifier.fillMaxSize())
    }
}

@OptIn(ExperimentalTvMaterialApi::class)
@Composable
fun rememberCarouselState(
    initialSlideIndex: Int,
) = remember(initialSlideIndex) {
    CarouselState(initialSlideIndex = initialSlideIndex)
}

```

こんな感じになります。

![Carouselをフルカスタマイズ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/79452e8b-42cf-213e-af20-70f0d937f573.gif)