カタログ ブラウザを作成する

TV で使用するメディア アプリでは、ユーザーが提供コンテンツをブラウズして選び、再生を開始できるようにする必要があります。このタイプのアプリのコンテンツ ブラウジングに関するユーザー エクスペリエンスは、シンプルで直観的、そして目を楽しませる魅力的なものである必要があります。

このレッスンでは、v17 leanback サポート ライブラリに含まれるクラスを使用して、アプリのメディア カタログから音楽や動画を閲覧するためのユーザー インターフェースを実装する方法について説明します。

アプリのメイン画面

図 1.Leanback のサンプルアプリ ブラウズ フラグメントによって、動画カタログのデータが表示されます。

メディア ブラウズ レイアウトを作成する

leanback サポート ライブラリの BrowseFragment クラスを使用すると、最小限のコードでブラウジング カテゴリのプライマリ レイアウトとメディア アイテムの行を作成できます。次の例では、BrowseFragment オブジェクトを含むレイアウトを作成する方法を示しています。

    <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);
        }
    ...
    

BrowseFragment メソッドによってビューに動画データと UI 要素が設定され、アイコン、タイトル、カテゴリ ヘッダーが有効になっているかどうかなどのレイアウト パラメータが設定されます。

次の例に示すように、BrowseFragment メソッドを実装するアプリケーションのサブクラスによって UI 要素に対するユーザー アクションのイベント リスナーも設定され、バックグラウンド マネージャーも準備されます。

Kotlin

    class MainFragment : BrowseFragment(),
            LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> {
        ...
        override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)

            loadVideoData()

            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 = resources.getColor(R.color.fastlane_background)
            // set search icon color
            searchAffordanceColor = resources.getColor(R.color.search_opaque)
        }

        private fun loadVideoData() {
            VideoProvider.setContext(activity)
            videosUrl = resources.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 BrowseFragment implements
            LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> {
    }
    ...

        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);

            loadVideoData();

            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(getResources().getColor(R.color.fastlane_background));
            // set search icon color
            setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));
        }

        private void loadVideoData() {
            VideoProvider.setContext(getActivity());
            videosUrl = getActivity().getResources().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() によって複数の BrowseFragment メソッドが呼び出されます。

  • 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">

カタログ ブラウザを表示する BrowseFragment 実装で、次の例に示すように 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();
        }
    });
    

例の一覧については、Leanback サンプルで IconHeaderItemPresenter をご覧ください。

ヘッダーを非表示または無効にする

スクロール可能なリストが必要なほどカテゴリが多くない場合など、行ヘッダーを表示する必要がない場合は、フラグメントの onActivityCreated() メソッド中に BrowseFragment.setHeadersState() メソッドを呼び出して行ヘッダーを非表示または無効にします。setHeadersState() メソッドは、次の定数のいずれかをパラメータとして与えられ、ブラウズ フラグメント内のヘッダーの初期状態を設定します。

  • HEADERS_ENABLED - ブラウズ フラグメント アクティビティが作成されるとヘッダーはデフォルトで有効になり、表示されます。ヘッダーはこのページの図 1 と図 2 のように表示されます。
  • HEADERS_HIDDEN - ブラウズ フラグメント アクティビティが作成されるとヘッダーはデフォルトで有効になり、非表示になります。カードビューを提供する図 1 のように、画面のヘッダー セクションは折りたたまれます。ユーザーは、折りたたまれたヘッダー セクションを選択して展開できます。
  • HEADERS_DISABLED - ブラウズ フラグメント アクティビティが作成されるとヘッダーはデフォルトで無効になり、表示されません。

HEADERS_ENABLED または HEADERS_HIDDEN が設定された場合、setHeadersTransitionOnBackEnabled() を呼び出して、行で選択されたコンテンツ アイテムから行ヘッダーに戻ることをサポートできます。これはデフォルトで有効ですが(メソッドを呼び出さない場合)、戻るという動作を自分で処理する場合は値 falsesetHeadersTransitionOnBackEnabled() に渡し、独自のバックスタック処理を実装する必要があります。

メディアリストを表示する

BrowseFragment クラスでは、アダプタとプレゼンターを使用してブラウズ可能なメディア コンテンツのカテゴリやメディア カタログのメディア アイテムを定義したり、表示したりできます。アダプタを使用すると、メディア カタログ情報が含まれているローカルやオンラインのデータソースに接続できます。アダプタはプレゼンターを使用してビューを作成し、アイテムを画面に表示するためにそれらのビューにデータをバインドします。

次のコード例では、文字列データを表示する際の 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.getContext().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
        }
    }
    

メディア アイテムのプレゼンター クラスを構築したら、アダプタをビルドしたり、BrowseFragment にアタッチしたりして、ユーザーのブラウジング用に画面上にそれらのアイテムを表示させることができます。次のサンプルコードでは、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))
            }
        }
        browseFragment.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));
        }

        browseFragment.setAdapter(rowsAdapter);
    }
    

この例では、アダプタの静的実装を示しています。一般的なメディア ブラウジング アプリは、オンライン データベースやウェブサービスのデータを使用します。ウェブから取得したデータを使用したブラウジング アプリの例については、Android Leanback のサンプル アプリを参照してください。

背景を切り替える

TV で使用されるメディア ブラウジング アプリを目立たせるために、ユーザーがコンテンツをブラウジングしている間に背景イメージを切り替えられます。これにより、アプリの使用がより動きのある楽しいものになります。

Leanback サポート ライブラリは、TV アプリのアクティビティの背景を変更するための BackgroundManager クラスを提供しています。次の例では、TV アプリのアクティビティ内の背景をアップデートする簡単なメソッドの作成方法を示しています。

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 つのアイテムに落ち着くまで背景イメージの変更を遅らせる時間を追加することも検討してください。これにより、背景イメージの切り替えが頻繁に発生しないようにできます。