Android TV用のJetpack Composeがリリースされました

# はじめに

これまでJetpack Composeには、通常のComposeの他にもWear OS用のComposeが存在していて、Android TV用のComposeが存在していなかったのですが、2022/10/05にalpha版がリリースされていたので、そちらのAPIを抜粋します。

https://developer.android.com/jetpack/androidx/releases/tv

# フォーカスについて
その前に、Android TVはPhoneなどと違い、タッチパネルも持たず、トラックボールなどを用いてのポインタ操作でのコンポーネントクリックも不可能なデバイスです。
Android TVの主な操作は、リモコンを操作してUIコンポーネントを移動して、決定ボタンで現在指し示す場所をクリックする操作です。
そのため、現在指し示す場所（フォーカス）がどこにあるかをユーザーに教えてあげる必要があります。

これを行うために、`FocusRequester`などでフォーカスを実装し、フォーカスされている箇所は色を変えるなど、周りより強調する対応が必要になります。
以下はフォーカスを指し示すLazyRowのコードです。

```kotlin
    LazyRow() {
        items(50) { index ->
            val focusRequester = remember { FocusRequester() }
            var backgroundColor by remember { mutableStateOf(Transparent) }
            Button(
                onClick = { /*TODO*/ },
                modifier = Modifier
                    .focusRequester(focusRequester)
                    .onFocusChanged {
                        if (it.hasFocus || it.isFocused) {
                            backgroundColor = Color.Red
                        } else {
                            backgroundColor = Color.Cyan
                        }
                    },
                colors = ButtonDefaults.buttonColors(
                    containerColor = backgroundColor
                ),

                ) {
                Text(text = "Item: $index")
            }
        }
    }
```

矢印キーで操作すると、ボタンがフォーカスすることがわかります。


![Focused LazyRow](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/1a9fc736-05cb-270c-151e-92d72927dbf8.gif)

# Android TV用のJetpack Compose API
## TvLazyRow
TV用のLazyRowです。
用途やAPIの引数もほぼLazyRowと同様の使い方ができます。
大きく違う点は`PivotOffsets`が考慮される点です。

PivotOffsetsというのは、視点の中心を示します。
先ほどのLazyRowのスクロールでは、フォーカスが右端にたどり着いたらスクロールが開始されます。
これはスクロール時、ユーザーが常にTVの右端に注目しないといけないことを意味していて、ユーザーとしては不便と感じる可能性があります。
このPivotOffsetsを設定することによって、スクロールがどこから開始するかを指定することができます。
TvLazyRowではデフォルトでPivotOffsetsが設定されますが、自分で調整も可能です。
他にも同様に、`TvLazyColumn`、`TvLazyHorizontalGrid`、`TvLazyVerticalGrid`も用意されています。


```kotlin
    TvLazyRow() {
        items(50) { index ->
            val focusRequester = remember { FocusRequester() }
            var backgroundColor by remember { mutableStateOf(Transparent) }
            Button(
                onClick = { /*TODO*/ },
                modifier = Modifier
                    .focusRequester(focusRequester)
                    .onFocusChanged {
                        if (it.hasFocus || it.isFocused) {
                            backgroundColor = Color.Red
                        } else {
                            backgroundColor = Color.Cyan
                        }
                    },
                colors = ButtonDefaults.buttonColors(
                    containerColor = backgroundColor
                ),

                ) {
                Text(text = "Item: $index")
            }
        }
    }
```

![TvLazyRow](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/df84f843-7b33-1d48-9f24-a4d8f3cc5d21.gif)

## ImmersiveList
ImmersiveListは、バックグラウンドと共にコンポーネントを提供するものになります。
よく使われるTVの挙動として、ボタンにフォーカスが当たった時に、背景にそのボタンが示す動画の一枚絵を表示するようなUIがあると思います。
それをImmersiveListで実現可能です。

まず、バックグラウンドには、ボタンにフォーカスが当たった時に表示する背景を設定します。
indexなどが渡されるので、インデックスに応じた背景を表示します。
以下のコードではAnimatedContentを用いてボタンのフォーカスが移り変わるたびに一枚絵を表示しています。

contentのlistには、フォーカス送信可能な、表示するコンポーネントを設定します。
コンポーネント中のmodifierに`focusableItem`を設定することでフォーカスが変わったことをbackgroundに伝えます。
そうすることで、スクロールでフォーカスが変わるたびに背景の変更が起こります。

```kotlin
@OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
@Composable
fun Greeting(name: String) {
    ImmersiveList(background = { index, listHasFocus ->
        AnimatedContent(targetState = index) {
            val data = when (index % 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())
        }
    }) {
        TvLazyRow() {
            items(50) { index ->
                val focusRequester = remember { FocusRequester() }
                var backgroundColor by remember { mutableStateOf(Transparent) }
                Button(
                    onClick = { /*TODO*/ },
                    modifier = Modifier
                        .focusableItem(index) //←追加
                        .focusRequester(focusRequester)
                        .onFocusChanged {
                            if (it.hasFocus || it.isFocused) {
                                backgroundColor = Color.Red
                            } else {
                                backgroundColor = Color.Cyan
                            }
                        },
                    colors = ButtonDefaults.buttonColors(
                        containerColor = backgroundColor
                    ),
                    ) {
                    Text(text = "Item: $index")
                }
            }
        }
    }
}
```

![ImmersiveList](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/e616ccbf-56f0-9ca2-5036-fb23d4adc403.gif)