はじめに
これまで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")
}
}
}
矢印キーで操作すると、ボタンがフォーカスすることがわかります。
Android TV用のJetpack Compose API
TvLazyRow
TV用のLazyRowです。
用途やAPIの引数もほぼLazyRowと同様の使い方ができます。
大きく違う点はPivotOffsets
が考慮される点です。
PivotOffsetsというのは、視点の中心を示します。
先ほどのLazyRowのスクロールでは、フォーカスが右端にたどり着いたらスクロールが開始されます。
これはスクロール時、ユーザーが常にTVの右端に注目しないといけないことを意味していて、ユーザーとしては不便と感じる可能性があります。
このPivotOffsetsを設定することによって、スクロールがどこから開始するかを指定することができます。
TvLazyRowではデフォルトでPivotOffsetsが設定されますが、自分で調整も可能です。
他にも同様に、TvLazyColumn
、TvLazyHorizontalGrid
、TvLazyVerticalGrid
も用意されています。
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")
}
}
}
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")
}
}
}
}
}