8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

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

フォーカスについて

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

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

    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

Android TV用のJetpack Compose API

TvLazyRow

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

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

    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

ImmersiveList

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

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

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

@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

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?