Android TV向けアプリを開発するとき、最初に戸惑うのは「UIと操作体系がスマホとまったく違う」という点です。
タッチ操作ではなく、リモコンによるフォーカス移動が前提となるため、通常のRecyclerViewベースUIはほぼ使い物になりません。
そのため、Android TVでは Leanbackライブラリ が事実上の標準UIフレームワークになっています。
本記事では、Leanbackを使って
・一覧画面を構築し
・フォーカス操作を実装し
・動画を選択して
・ExoPlayerで再生する
という一連の流れを、設計意図と実装コードの両面から解説します。
なぜLeanbackを使うのか
Android TVでは次の制約があります。
- 入力は上下左右+決定キーのみ
- 画面までの距離が遠い
- フォーカスが常に可視である必要がある
- 一度に多くの情報を詰め込めない
Leanbackはこれらを前提に
- 横スクロール中心のレイアウト
- フォーカス状態の自動管理
- カードUIの標準化
- 遷移アニメーションの内蔵
を提供します。
全体アーキテクチャ
[BrowseSupportFragment]
↓ 選択
[PlayerActivity]
↓ 再生
[ExoPlayer]
役割分離は以下の通りです。
| レイヤ | 役割 |
|---|---|
| Leanback | UIとフォーカス制御 |
| Activity / Fragment | 画面遷移 |
| ExoPlayer | 動画再生 |
依存関係
dependencies {
implementation "androidx.leanback:leanback:1.2.0"
implementation "androidx.media3:media3-exoplayer:1.3.1"
implementation "androidx.media3:media3-ui:1.3.1"
}
データモデル
data class VideoItem(
val title: String,
val description: String,
val url: String
)
一覧画面(BrowseSupportFragment)
class MainFragment : BrowseSupportFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
title = "動画一覧"
val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
val cardAdapter = ArrayObjectAdapter(CardPresenter())
cardAdapter.add(
VideoItem(
"Big Buck Bunny",
"オープンソースのサンプル動画",
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
)
)
rowsAdapter.add(ListRow(HeaderItem(0, "サンプル"), cardAdapter))
adapter = rowsAdapter
onItemViewClickedListener = OnItemViewClickedListener { _, item, _, _ ->
val video = item as VideoItem
startActivity(PlayerActivity.newIntent(requireContext(), video))
}
}
}
CardPresenter
class CardPresenter : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.card_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
val video = item as VideoItem
viewHolder.view.findViewById<TextView>(R.id.title).text = video.title
viewHolder.view.findViewById<TextView>(R.id.desc).text = video.description
}
override fun onUnbindViewHolder(viewHolder: ViewHolder) {}
}
再生画面
class PlayerActivity : FragmentActivity() {
private lateinit var player: ExoPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val playerView = PlayerView(this)
setContentView(playerView)
val video = intent.getParcelableExtra<VideoItem>("video")!!
player = ExoPlayer.Builder(this).build()
playerView.player = player
player.setMediaItem(MediaItem.fromUri(video.url))
player.prepare()
player.play()
}
override fun onStop() {
super.onStop()
player.release()
}
companion object {
fun newIntent(context: Context, video: VideoItem) =
Intent(context, PlayerActivity::class.java).putExtra("video", video)
}
}
フォーカスとUX上の注意点
- Viewは必ず focusable にする
- フォーカス時にスケールアップすると操作感が向上
- フォーカス遷移が飛ばないようカードサイズを揃える
よくある問題
| 問題 | 原因 |
|---|---|
| フォーカスが当たらない | focusable属性未設定 |
| リモコン操作不能 | タッチ前提UI |
| 動画が止まらない | onStopでreleaseしていない |
まとめ
LeanbackとExoPlayerを使えば、Android TV向け動画アプリは
・UI設計
・フォーカス制御
・再生処理
をそれぞれ分離しつつ、安全に構築できます。
TVはスマホの延長ではなく、別カテゴリのUXであるという前提を受け入れることが成功の鍵になります。