テレビで使用するメディアアプリでは、ユーザーが提供コンテンツをブラウズして選び、再生を開始できるようにする必要があります。このタイプのアプリのコンテンツ ブラウジングに関するユーザー エクスペリエンスは、シンプルで直観的、そして目を楽しませる魅力的なものである必要があります。
このレッスンでは、Leanback androidx ライブラリに含まれるクラスを使用して、アプリのメディア カタログから音楽や動画を閲覧するためのユーザー インターフェースを実装する方法について説明します。
注: ここに示す実装例では、サポートが終了した BrowseFragment
クラスではなく、BrowseSupportFragment
を使用しています。BrowseSupportFragment
は AndroidX の Fragment
クラスを拡張します。これにより、デバイスと Android のバージョン間で一貫した動作が保証されます。

図 1. Leanback のサンプルアプリ ブラウズ フラグメントによって、動画カタログのデータが表示されます。
メディア ブラウズ レイアウトを作成する
Leanback サポート ライブラリの BrowseSupportFragment
クラスを使用すると、最小限のコードでブラウジング カテゴリのプライマリ レイアウトとメディア アイテムの行を作成できます。次の例は、BrowseSupportFragment
オブジェクトを含むレイアウトを作成する方法を示しています。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/main_frame" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:name="com.example.android.tvleanback.ui.MainFragment" android:id="@+id/main_browse_fragment" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
次の例に示すように、このビューはアプリケーションのメイン アクティビティによって設定されます。
Kotlin
class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) } ...
Java
public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } ...
BrowseSupportFragment
メソッドによってビューに動画データと UI 要素が設定され、アイコン、タイトル、カテゴリ ヘッダーが有効になっているかどうかなどのレイアウト パラメータが設定されます。
- UI 要素の設定の詳細については、UI 要素を設定するをご覧ください。
- ヘッダーを非表示にする方法については、ヘッダーを非表示または無効にするをご覧ください。
次の例に示すように、BrowseSupportFragment
メソッドを実装するアプリケーションのサブクラスによって UI 要素に対するユーザー アクションのイベント リスナーも設定され、バックグラウンド マネージャーも準備されます。
Kotlin
class MainFragment : BrowseSupportFragment(), LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) loadVideoData() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) prepareBackgroundManager() setupUIElements() setupEventListeners() } ... private fun prepareBackgroundManager() { backgroundManager = BackgroundManager.getInstance(activity).apply { attach(activity?.window) } defaultBackground = resources.getDrawable(R.drawable.default_background) metrics = DisplayMetrics() activity?.windowManager?.defaultDisplay?.getMetrics(metrics) } private fun setupUIElements() { badgeDrawable = resources.getDrawable(R.drawable.videos_by_google_banner) // Badge, when set, takes precedent over title title = getString(R.string.browse_title) headersState = BrowseSupportFragment.HEADERS_ENABLED isHeadersTransitionOnBackEnabled = true // set headers background color brandColor = ContextCompat.getColor(requireContext(), R.color.fastlane_background) // set search icon color searchAffordanceColor = ContextCompat.getColor(requireContext(), R.color.search_opaque) } private fun loadVideoData() { VideoProvider.setContext(activity) videosUrl = getString(R.string.catalog_url) loaderManager.initLoader(0, null, this) } private fun setupEventListeners() { setOnSearchClickedListener { Intent(activity, SearchActivity::class.java).also { intent -> startActivity(intent) } } onItemViewClickedListener = ItemViewClickedListener() onItemViewSelectedListener = ItemViewSelectedListener() } ...
Java
public class MainFragment extends BrowseSupportFragment implements LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> { } ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); loadVideoData(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); prepareBackgroundManager(); setupUIElements(); setupEventListeners(); } ... private void prepareBackgroundManager() { backgroundManager = BackgroundManager.getInstance(getActivity()); backgroundManager.attach(getActivity().getWindow()); defaultBackground = getResources() .getDrawable(R.drawable.default_background); metrics = new DisplayMetrics(); getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); } private void setupUIElements() { setBadgeDrawable(getActivity().getResources() .getDrawable(R.drawable.videos_by_google_banner)); // Badge, when set, takes precedent over title setTitle(getString(R.string.browse_title)); setHeadersState(HEADERS_ENABLED); setHeadersTransitionOnBackEnabled(true); // set headers background color setBrandColor(ContextCompat.getColor(requireContext(), R.color.fastlane_background)); // set search icon color setSearchAffordanceColor(ContextCompat.getColor(requireContext(), R.color.search_opaque)); } private void loadVideoData() { VideoProvider.setContext(getActivity()); videosUrl = getString(R.string.catalog_url); getLoaderManager().initLoader(0, null, this); } private void setupEventListeners() { setOnSearchClickedListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(getActivity(), SearchActivity.class); startActivity(intent); } }); setOnItemViewClickedListener(new ItemViewClickedListener()); setOnItemViewSelectedListener(new ItemViewSelectedListener()); } ...
UI 要素を設定する
上記のサンプルでは、メディア カタログ ブラウザのスタイルを指定するためにプライベート メソッド setupUIElements()
によって複数の BrowseSupportFragment
メソッドが呼び出されます。
setBadgeDrawable()
は図 1 と図 2 に示すように、指定されたドローアブル リソースをブラウズ フラグメントの右上隅に配置します。setTitle()
も呼び出された場合、このメソッドはタイトル文字列をドローアブル リソースに置き換えます。ドローアブル リソースの高さは 52 dps である必要があります。setTitle()
は、setBadgeDrawable()
が呼び出されない限り、タイトル文字列をブラウズ フラグメントの右上隅に設定します。setHeadersState()
とsetHeadersTransitionOnBackEnabled()
は、ヘッダーを非表示または無効にします。 詳しくは、ヘッダーを非表示または無効にするをご覧ください。setBrandColor()
は、ブラウズ フラグメント内の UI 要素の背景色(特にヘッダー セクションの背景の色)を指定された色の値を使用して設定します。setSearchAffordanceColor()
は、検索アイコンの色を指定された色の値を使用して設定します。図 1 と図 2 に示すように、検索アイコンはブラウズ フラグメントの左上隅に表示されます。
ヘッダービューをカスタマイズする
図 1 に示すブラウズ フラグメントでは、動画カテゴリ名(行ヘッダー)が左ペインに表示されます。テキストビューには、これらのカテゴリ名が動画データベースから表示されます。より複雑なレイアウトでその他のビューが含まれるようにヘッダーをカスタマイズできます。ここでは、図 2 に示すようにカテゴリ名の横にアイコンを表示する画像ビューを含める方法について説明します。

図 2. ブラウズ フラグメント内の行ヘッダー。アイコンとテキストラベルの両方があります。
行ヘッダーのレイアウトは次のように定義されます。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/header_icon" android:layout_width="32dp" android:layout_height="32dp" /> <TextView android:id="@+id/header_label" android:layout_marginTop="6dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
Presenter
を使用して、ビューホルダーを作成、バインド、アンバインドする抽象メソッドを実装します。次の例は、2 つのビュー ImageView
および TextView
を含むビューホルダーをバインドする方法を示しています。
Kotlin
class IconHeaderItemPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder { val view = LayoutInflater.from(viewGroup.context).run { inflate(R.layout.icon_header_item, null) } return Presenter.ViewHolder(view) } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) { val headerItem = (o as ListRow).headerItem val rootView = viewHolder.view rootView.findViewById<ImageView>(R.id.header_icon).apply { rootView.resources.getDrawable(R.drawable.ic_action_video, null).also { icon -> setImageDrawable(icon) } } rootView.findViewById<TextView>(R.id.header_label).apply { text = headerItem.name } } override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) { // no op } }
Java
public class IconHeaderItemPresenter extends Presenter { @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup) { LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); View view = inflater.inflate(R.layout.icon_header_item, null); return new ViewHolder(view); } @Override public void onBindViewHolder(ViewHolder viewHolder, Object o) { HeaderItem headerItem = ((ListRow) o).getHeaderItem(); View rootView = viewHolder.view; ImageView iconView = (ImageView) rootView.findViewById(R.id.header_icon); Drawable icon = rootView.getResources().getDrawable(R.drawable.ic_action_video, null); iconView.setImageDrawable(icon); TextView label = (TextView) rootView.findViewById(R.id.header_label); label.setText(headerItem.getName()); } @Override public void onUnbindViewHolder(ViewHolder viewHolder) { // no op } }
ヘッダーは、D-pad を使用してスクロールできるようフォーカス可能でなければなりません。方法には 2 つあります。
onBindViewHolder()
でビューをフォーカス可能に設定します。Kotlin
override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) { val headerItem = (o as ListRow).headerItem val rootView = viewHolder.view rootView.focusable = View.FOCUSABLE //... }
Java
@Override public void onBindViewHolder(ViewHolder viewHolder, Object o) { HeaderItem headerItem = ((ListRow) o).getHeaderItem(); View rootView = viewHolder.view; rootView.setFocusable(View.FOCUSABLE) // Allows the D-Pad to navigate to this header item //... }
- レイアウトをフォーカス可能に設定します。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
最後に、カタログ ブラウザを表示する BrowseSupportFragment
実装で、次の例に示すように setHeaderPresenterSelector()
メソッドを使用して行ヘッダーのプレゼンターを設定します。
Kotlin
setHeaderPresenterSelector(object : PresenterSelector() { override fun getPresenter(o: Any): Presenter { return IconHeaderItemPresenter() } })
Java
setHeaderPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object o) { return new IconHeaderItemPresenter(); } });
例の詳細については、Android TV GitHub リポジトリにある Android Leanback サンプルアプリをご覧ください。
ヘッダーを非表示または無効にする
スクロール可能なリストが必要なほどカテゴリが多くない場合など、行ヘッダーを表示する必要がない場合は、フラグメントの onActivityCreated()
メソッド中に BrowseSupportFragment.setHeadersState()
メソッドを呼び出して、行ヘッダーを非表示または無効にします。setHeadersState()
メソッドは、次の定数のいずれかをパラメータとして与えられ、ブラウズ フラグメント内のヘッダーの初期状態を設定します。
HEADERS_ENABLED
- ブラウズ フラグメント アクティビティが作成されると、ヘッダーはデフォルトで有効になり、表示されます。ヘッダーはこのページの図 1 と図 2 のように表示されます。HEADERS_HIDDEN
- ブラウズ フラグメント アクティビティが作成されると、ヘッダーはデフォルトで有効になり、非表示になります。カードビューを提供するの図 1 のように、画面のヘッダー セクションは折りたたまれます。ユーザーは、折りたたまれたヘッダー セクションを選択して展開できます。HEADERS_DISABLED
- ブラウズ フラグメント アクティビティが作成されると、ヘッダーはデフォルトで無効になり、表示されません。
HEADERS_ENABLED
または HEADERS_HIDDEN
が設定された場合、setHeadersTransitionOnBackEnabled()
を呼び出して、行で選択されたコンテンツ アイテムから行ヘッダーに戻る動作をサポートできます。これはデフォルトで有効ですが(メソッドを呼び出さない場合)、戻るという動作を自分で処理する場合は、値 false
を setHeadersTransitionOnBackEnabled()
に渡し、独自のバックスタック処理を実装する必要があります。
メディアリストを表示する
BrowseSupportFragment
クラスでは、アダプターとプレゼンターを使用して、ブラウズ可能なメディア コンテンツのカテゴリやメディア カタログのメディア アイテムを定義したり表示したりできます。アダプターを使用すると、メディア カタログ情報が含まれているローカルやオンラインのデータソースに接続できます。
アダプターはプレゼンターを使用してビューを作成し、アイテムを画面に表示するためにそれらのビューにデータをバインドします。
次のコード例は、文字列データを表示する際の Presenter
の実装を示しています。
Kotlin
private const val TAG = "StringPresenter" class StringPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): Presenter.ViewHolder { val textView = TextView(parent.context).apply { isFocusable = true isFocusableInTouchMode = true background = parent.resources.getDrawable(R.drawable.text_bg) } return Presenter.ViewHolder(textView) } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) { (viewHolder.view as TextView).text = item.toString() } override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) { // no op } }
Java
public class StringPresenter extends Presenter { private static final String TAG = "StringPresenter"; public ViewHolder onCreateViewHolder(ViewGroup parent) { TextView textView = new TextView(parent.getContext()); textView.setFocusable(true); textView.setFocusableInTouchMode(true); textView.setBackground( parent.getResources().getDrawable(R.drawable.text_bg)); return new ViewHolder(textView); } public void onBindViewHolder(ViewHolder viewHolder, Object item) { ((TextView) viewHolder.view).setText(item.toString()); } public void onUnbindViewHolder(ViewHolder viewHolder) { // no op } }
メディア アイテムのプレゼンター クラスを構築すると、アダプターをビルドしたり、BrowseSupportFragment
にアタッチしたりして、ユーザーのブラウジング用に画面上にそれらのアイテムを表示させることができます。次のサンプルコードでは、1 つ前のコード例に示したように、StringPresenter
クラスを使用してカテゴリとそのカテゴリ内のアイテムを表示するためのアダプターの構築方法を示しています。
Kotlin
private const val NUM_ROWS = 4 ... private lateinit var rowsAdapter: ArrayObjectAdapter override fun onCreate(savedInstanceState: Bundle?) { ... buildRowsAdapter() } private fun buildRowsAdapter() { rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) for (i in 0 until NUM_ROWS) { val listRowAdapter = ArrayObjectAdapter(StringPresenter()).apply { add("Media Item 1") add("Media Item 2") add("Media Item 3") } HeaderItem(i.toLong(), "Category $i").also { header -> rowsAdapter.add(ListRow(header, listRowAdapter)) } } browseSupportFragment.adapter = rowsAdapter }
Java
private ArrayObjectAdapter rowsAdapter; private static final int NUM_ROWS = 4; @Override protected void onCreate(Bundle savedInstanceState) { ... buildRowsAdapter(); } private void buildRowsAdapter() { rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); for (int i = 0; i < NUM_ROWS; ++i) { ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter( new StringPresenter()); listRowAdapter.add("Media Item 1"); listRowAdapter.add("Media Item 2"); listRowAdapter.add("Media Item 3"); HeaderItem header = new HeaderItem(i, "Category " + i); rowsAdapter.add(new ListRow(header, listRowAdapter)); } browseSupportFragment.setAdapter(rowsAdapter); }
この例では、アダプターの静的実装を示しています。一般的なメディア ブラウジング アプリは、オンライン データベースやウェブサービスのデータを使用します。ウェブから取得したデータを使用するブラウジング アプリの例については、Android TV GitHub リポジトリにある Android Leanback サンプルアプリをご覧ください。
背景を切り替える
テレビで使用されるメディア ブラウジング アプリを目立たせるために、ユーザーがコンテンツをブラウジングしている間に背景イメージを切り替えられます。これにより、アプリの使用がより動きのある楽しいものになります。
Leanback サポート ライブラリは、テレビアプリのアクティビティの背景を変更するための BackgroundManager
BackgroundManager クラスを提供しています。次の例では、テレビアプリのアクティビティ内の背景をアップデートする簡単なメソッドの作成方法を示しています。
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
Java
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
既存のほとんどのメディア ブラウズ アプリでは、ユーザーがメディアリストをスクロールするのに合わせて自動的に背景が切り替えられます。これを行うには選択リスナーを設定して、ユーザーの現在の選択に基づいて自動的に背景を変更できるようにします。次の例は、OnItemViewSelectedListener
クラスをセットアップして、選択イベントの判別後に背景を変更する方法を示しています。
Kotlin
protected fun clearBackground() { BackgroundManager.getInstance(this).drawable = defaultBackground } protected fun getDefaultItemViewSelectedListener(): OnItemViewSelectedListener = OnItemViewSelectedListener { _, item, _, _ -> if (item is Movie) { item.getBackdropDrawable().also { background -> updateBackground(background) } } else { clearBackground() } }
Java
protected void clearBackground() { BackgroundManager.getInstance(this).setDrawable(defaultBackground); } protected OnItemViewSelectedListener getDefaultItemViewSelectedListener() { return new OnItemViewSelectedListener() { @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (item instanceof Movie ) { Drawable background = ((Movie)item).getBackdropDrawable(); updateBackground(background); } else { clearBackground(); } } }; }
注: 上記の実装例は、わかりやすくするために実際よりも単純化されています。実際にアプリでこの機能を作成する際は、パフォーマンス向上のために、別のスレッドで背景の変更アクションを実行するようにしてください。また、ユーザーがアイテムをスクロールする動作に合わせて背景を切り替える場合には、ユーザーが 1 つのアイテムに落ち着くまで背景イメージの変更を遅らせる時間を追加することも検討してください。これにより、背景イメージの切り替えが頻繁に発生しないようにできます。