【Android TV】Leanbackの画面でチェックボックスを使う

こんにちは。仙台オフィスでスマホアプリの開発などを担当している、はんだです。最近はスマホアプリだけでなく、Android TVアプリも触らせてもらってます。

VideoMarketのAndroid TVアプリはLeanbackを利用して作成していますが、表示している作品一覧から複数の作品を選択させたいことがありました。 具体的には、表示されているお気に入り一覧から、選択した作品をお気に入りから除外する機能を実装するために必要でした。

VideoMarketのお気に入り編集画面(本記事執筆時点では未リリース)

このためにLeanback画面にチェックボックスを持たせたのですが、実装の際に色々と詰まることがあったので、今回はそれについて書きたいと思います。

はじめに

Android TVの公式サンプルコードの中にReferenceAppKotlinというものがあり、BrowseSupportFragmentを使った画面が含まれているので、これをベースに説明したいと思います。

github.com

こちらのブラウズ画面に、テキストだけを表示しているカードがあるのですが、これをチェックボックス付きのカードに変更したいと思います。

これをチェックボックス付きのカードにする

チェックボックスの表示

チェックボックスを表示すること自体は簡単です。presenter_menu_item.xmlを修正して、チェックボックスを含むようにします。

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/menu_item_width"
    android:layout_height="@dimen/menu_item_height"
    android:focusable="true"
    android:focusableInTouchMode="true"
    >
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/title"
        android:layout_width="@dimen/menu_item_width"
        android:layout_height="@dimen/menu_item_height"
        android:gravity="center"
        android:background="@color/gray_dark"
        android:textColor="@color/screen_white" />
    <CheckBox
        android:id="@+id/checkbox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</FrameLayout>

こちらのCustomMenuItemPresenterですが、Firebaseとの連携用のカードが表示されています。この記事では使わないので、BrowseFragmentonCreate(savedInstanceState: Bundle?)内のviewModel.isSignedIn.observe部分をコメントアウトします。

代わりに、チェックボックス付きのカードと、チェックされたアイテムを保持するリストを作ります。

 private val checkedItems = mutableListOf<String>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        listOf(
            BrowseCustomMenu(
                "複数選択デモ",
                listOf(
                    BrowseCustomMenu.MenuItem("アイテム1") {},
                    BrowseCustomMenu.MenuItem("アイテム2") {},
                    BrowseCustomMenu.MenuItem("アイテム3") {},
                    BrowseCustomMenu.MenuItem("アイテム4") {},
                    BrowseCustomMenu.MenuItem("アイテム5") {},
                )
            )
        ).also {
            viewModel.customMenuItems.postValue(it)
        }
    }

レイアウトファイルの構成を変えたのでCustomMenuItemPresenterを修正する必要があります。

    override fun onBindViewHolder(
        viewHolder: ViewHolder,
        item: Any?
    ) {
        checkNotNull(item)
        val binding = PresenterMenuItemBinding.bind(viewHolder.view)
        // binding.root.text = item.toString()    コメントアウト

        val menuItem = item as? BrowseCustomMenu.MenuItem
        binding.title.text = item.toString()
    }

ここまでで、チェックボックスのついたカードの表示までできているはずです。

チェックボックスが表示される

チェックをつける

さて、先ほどの修正でチェックボックスは表示されるようになりますが、カードを選択してもチェックボックスにチェックがつきません。

これについては、BrowseSupportFragmentOnItemViewClickedListenerで選択されたアイテムのViewHolderが取得できるので、その中でチェックをトグルさせます。

サンプルコードのBrowseFragmentに既にsetOnItemViewClickedListenerがあるので、ここに追記します。先ほど用意したcheckedItemsの中身もここでセットします。

setOnItemViewClickedListener { viewHolder, item, _, _ ->    // viewHolderを追記
    when (item) {
        is Video -> (略)
        is BrowseCustomMenu.MenuItem -> {
            viewHolder.view.findViewById<CheckBox>(R.id.checkbox).also {
                it.isChecked = !it.isChecked
                if (it.isChecked) {
                    checkedItems.add(item.toString())
                } else {
                    checkedItems.remove(item.toString())
                }
            }
        }
    }
}

これで、カードを選択するとチェックがトグルするようになります。

さて、ここまででチェック機能はできたのですが、一つ問題があります。それはカードが再描画された際にチェックが消えたりずれたりすることです。カードにチェックをした後、ブラウズ画面を上下にスクロールさせると再現すると思います。

Bind時にチェック済みかどうか判定する

再描画されてもチェックが消えたりずれたりしないように、CustomMenuItemPresenteronBindViewHolder内でカードがチェック済みかどうか判定し、それに応じてチェックをつけるようにします。

    val binding = PresenterMenuItemBinding.bind(viewHolder.view)
    // binding.root.text = item.toString()    コメントアウト

    val menuItem = item as? BrowseCustomMenu.MenuItem
    binding.title.text = item.toString()
    // コンストラクタでBrowseFragmentのcheckedItemListを渡す
    binding.checkbox.isChecked = checkedItemList.contains(menuItem.toString())    // 追加箇所

この例ではCustomMenuItemPresenterのコンストラクタで先ほどのcheckedItemListを受け取っています。

これで、上下にスクロールさせてもチェックボックスの状態が変わらなくなります。

まとめ

以上、Leanbackの画面でチェックボックスを使う際の手順を書かせていただきました。まとめると

  • レイアウトファイルにCheckBoxを追加する
  • BrowseSupportFragmentsetOnItemViewClickedListenerからviewHolderを取得し、チェックを付け替える
  • プレゼンターのonBindViewHolderで、チェック済みかどうか判定する

という流れになります。また、本記事ではBrowseSupportFragmentを例に説明しましたが、VerticalGridSupportFragmentでも同様に動きます。

今回は以上になります。ここまでお読みいただきありがとうございました。