7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Android TVのLeanbackで動画を再生してみた 〜UI設計から再生制御までの実践記録〜

Posted at

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であるという前提を受け入れることが成功の鍵になります。

7
0
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
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?