Android TV(工事中)

SIシステムをAndroid TVの搭載する検討を行います。

Android TVとは

Android TV

Android TVを使うには、「Nexus Player」であろう。

コンテンツ配信サービスの充実と言う意味では、「Nexus Player」は「Amazon fire TV」よりショボイ感じがする。

しかし、端末アプリを開発すると言う意味では、Android OSのバージョンが古い「Amazon fire TV」の方がショボイので、 まずは、Android OSのバージョンが最新の「Nexus Player」で試験するのが良さそうだ。

ListRow, ListRowPresenter

Android TVの操作は、左右上下しか動かないリモコンだから、メニューは水平方向にコンテンツを並べるListRow/ListRowPresenterを 垂直方向に並べて使用することになる。

スマホ/タブレットで愛用したListViewとおさらばして、GridViewを使用することになる訳だ。

Introduction

Android leanback support libraryにはコンテンツ表示用のAPIがいくつか備わっている.

ここではコンテンツを水平方向に並べるAPIについて記載する.

Overview

AndroidTVのデザインガイドラインには, 画面上下はカテゴリ等のセクション領域として確保する方針が書かれている.

そのためコンテンツのリストは垂直展開するListViewより水平展開するGridViewが好まれる.


[カテゴリ毎のメディアコンテンツリスト]

Horizontal list

Android leanback support libraryではコンテンツをリスト管理するためのクラスを提供する.

  • ListRow
  • ListRowPresenter

下記はこれらを使用したサンプルコード.

private ArrayObjectAdapter mRowsAdapter;
private static final int NUM_ROWS = 4;

private void buildRowsAdapter() {
    mRowsAdapter = 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, null);
        mRowsAdapter.add(new ListRow(header, listRowAdapter));
    }

    mBrowseFragment.setAdapter(mRowsAdapter);
}

ListRowはRowのサブクラスであるため, HeaderItemを持つ.

更にHeaderItemに紐づくコンテンツを子要素として管理するよう拡張されている.

下図はListRowの構造.


[ListRow]

ListRowは子要素をObjectArrayで保持する.

上記サンプルコードではStringとStringPresenterのセット(listRowAdapter)になる.

※StringPresenterは別途用意したString専用のPresenter

下図は子要素の構造.


[Child Elements]

これら行リストは水平方向へ展開するためにしばしばHorizontalGridViewにバインドされる.

Vertical list

ListRowは行のリスト表現である.

ここにヘッダ(カテゴリ等)を追加し, 列リストを形成するためにListRowをArrayObjectAdapterで管理する.

これにより複数行リスト(列×行)を作成し, ListRowを列リストとして表現する.

ListRowがModelで, ListRowPresenterがPresenterとして作用する.

最終的なデータ構造は次の通り.


[ListRows]

これら列リストは垂直方向へ展開するためにしばしばVertialGridViewにバインドされる.

ListRow, ListRowPresenterのAPIs

ListRow

android.support.v17.leanback.widget.Row
 ┗android.support.v17.leanback.widget.ListRow
Class Overview
A row composed of a optional HeaderItem, and an ObjectAdapter describing children.

HeaderItem(任意)と, 子要素が定義されたObjectAdapterから構成される.

Summary
  • ListRow(HeaderItem header, ObjectAdapter adapter)
  • (no discription)
    

    ListRowを初期化する. 行のメタ情報となるHeaderItemと, コンテンツをリスト表示するObjectAdapterを引数に取る

  • getAdapter()
  • Get the ObjectAdapter that represents a list of objects.
    

    コンテンツをリスト表示するObjectAdapterを返却する

ListRowPresenter

android.support.v17.leanback.widget.Presenter
 ┗android.support.v17.leanback.widget.RowPresenter
   ┗android.support.v17.leanback.widget.ListRowPresenter
Class Overview
ListRowPresenter renders ListRow using a HorizontalGridView hosted in a ListRowView.

ListRowPresenterはListRowをレンダリングする. ListRowはHorizontalGridViewを持つListRowViewにバインドされる.

Hover card
Optionally, setHoverCardPresenterSelector(PresenterSelector) can be used to display a view for the currently focused list item below the rendered list. This view is known as a hover card.

任意で, フォーカスされたアイテムのPresenterSelectorを指定するsetHoverCardPresenterSelector(PresenterSelector)が利用できる.

例えばフォーカスしたアイテムをHover cardのように見せることができる.

Selection animation
ListRowPresenter disables RowPresenter's default dimming effect and draw a dim overlay on top of each individual child items. Subclass may override and disable isUsingDefaultListSelectEffect() and write its own dim effect in onSelectLevelChanged(RowPresenter.ViewHolder).

ListRowPresenterはRowPresenterの標準エフェクトを無効化し, フォーカスされていない各行のコンテンツリストにオーバレイの効果を描画する.

サブクラスはisUsingDefaultListSelectEffect()をoverrideしてこれを無効化し, onSelectLevelChanged(RowPresenter.ViewHolder)でエフェクトを再定義できる.

Shadow
ListRowPresenter applies a default shadow to child of each view. Call setShadowEnabled(boolean) to disable shadow. Subclass may override and return false in isUsingDefaultShadow() and replace with its own shadow implementation.

ListRowPresenterは標準でViewに影の効果を与える. setShadowEnabled(boolean)でこれを無効化できる.

サブクラスはisUsingDefaultShadow()でfalseを返し,独自の影の効果を実装できる.

Summary

下記は主なAPI.

  • ListRowPresenter (int zoomFactor)
  • Constructs a ListRowPresenter with the given parameters.
    zoomFactor: Controls the zoom factor used when an item view is focused. One of ZOOM_FACTOR_NONE, ZOOM_FACTOR_SMALL, ZOOM_FACTOR_MEDIUM, ZOOM_FACTOR_LARGE
    

    ListRowPresenterを初期化する.

    引数zoomFactorにはViewがフォーカスされた際のズーム係数を指定する. 係数は次の中から選択する(ZOOM_FACTOR_NONE, ZOOM_FACTOR_SMALL, ZOOM_FACTOR_MEDIUM, ZOOM_FACTOR_LARGE)

  • setHoverCardPresenterSelector(PresenterSelector selector)
  • Set PresenterSelector used for showing a select object in a hover card.
    

    選択されたアイテムをHover cardとして表示するためのPresenterSelectorを設定する

  • getHoverCardPresenterSelector()
  • Get PresenterSelector used for showing a select object in a hover card.
    

    選択されたアイテムをHolver cardとして表示するためのPresenterSelectorを取得する

  • getShadowEnabled()
  • Returns true if child shadow is enabled.
    

    子要素の影効果が有効になっている場合はtrueを返却する

  • setShadowEnabled(boolean enabled)
  • Enable or disable child shadow.
    

    子要素の影効果を有効/無効で設定する

  • isUsingDefaultShadow()
  • Returns true if SDK >= 18, where default shadow is applied to each individual child of HorizontalGridView.
    

    SDKバージョンが18以上の場合にtrueを返却する. 標準の影効果はHorizontalGridViewの子要素として個別に充てられる.

Model-View-Presenter Pattern

Model-View-Presenter Patternとは

Android TVでは、「Android leanback support library」なるライブラリが提供される。

「Android leanback support library」は、Model-View-Presenter Patternをサポートしており、 コンテンツ(Model)と表示(View)の責務を分離するPresenterの仕組みを提供する。

またModelとPresenterの接続役となるObjectAdapterも提供される。

ObjectAdapterとは、データ(メディアコンテンツ等)と, PresenterSelectorを内包する抽象クラスである。

BrowserFragment, DetailsFragment(あるいはそれらのサブクラス)のsetAdapter(ObjectAdapter)でObjectAdapterを登録する。

PresenterSelectorはModel毎に最適なPresenterを選択し, PresenterがこれをViewにバインドする。

Passive View

  • user events
  • update view

Presenter

  • user events
  • update view

Model

  • updates model
  • state-change events


Model-View-Presenter Pattern

Model-View-Presenter PatternのAPIs

ObjectAdapter

android.support.v17.leanback.widget.ObjectAdapter

Class Overview
Adapter for leanback activities. Provides access to a data model and is decoupled from the presentation of the items via PresenterSelector.

Leanback Activity用のAdapter. Modelへのアクセス手段を提供し, PresenterSelectorでModelとViewを分離する.

ObjectAdapterは抽象クラスであり, Modelを管理する手段を提供しない.
ModelをArrayListで管理する場合はサブクラスのArrayObjectAdapterを使用する.

// Set Presenter
ArrayObjectAdapter adapter =
        new ArrayObjectAdapter(new MyStringPresenter());

// Add Models
adapter.add("Media Item 1");
adapter.add("Media Item 2");
adapter.add("Media Item 3");

ObjectAdapterにPresenterSelectorを設定する主な方法は次の3パターン.

  • ObjectAdapterのコンストラクタ引数にPresenterSelectorインスタンスを渡す
  • ObjectAdapterのコンストラクタ引数にPresenterインスタンスを渡す
  • ObjectAdapter.setPresenterSelectorメソッドでPresenterSelectorをセットする
// 1. Set Presenter
ArrayObjectAdapter adapter1 = new ArrayObjectAdapter(
        new MyStringPresenter());

// 2. Set PresenterSelector
ArrayObjectAdapter adapter2 = new ArrayObjectAdapter(
        new SinglePresenterSelector(new MyStringPresenter()));

// 3. Lazy initialize
ArrayObjectAdapter adapter3 = new ArrayObjectAdapter();
adapter3.setPresenterSelector(
        new SinglePresenterSelector(new MyStringPresenter()));
Summary

下記は主なAPI.

  • ObjectAdapter(PresenterSelector presenterSelector)
  • Construct an adapter with the given PresenterSelector.
    

    Adapterを初期化し, PresenterSelectorを設定する

  • ObjectAdapter(Presenter presenter)
  • Construct an adapter that uses the given Presenter for all items.
    

    Adapterを初期化し, 全てのリスト項目にPresenterを使用する.内部的にはSinglePresenterSelectorが設定される.

  • ObjectAdapter.DataObserver
  • A DataObserver can be notified when an ObjectAdapter’s underlying data changes.
    

    DataObserverはObjectAdapterのデータに変更があった場合に通知を受け取ることができる

  • get(int position)
  • Returns the item for the given position.
    

    引数positionに位置するitem(Model)を返却

  • getId(int position)
  • Returns id for the given position.
    

    引数positionに位置するidを返却

  • getPresenter(Object item)
  • Returns the Presenter for the given item from the adapter.
    

    item(Model)に対応するPresenterを返却

  • setPresenterSelector(PresenterSelector presenterSelector)
  • Set the presenter selector.
    

    PresenterSelectorを設定する

PresenterSelector

BrowseFragment

Leanback support libraryはTVでバイスでメディアカタログを表示・閲覧するためのAPIをいくつか提供する.

音楽,ビデオを閲覧するためのUIを提供するこれらAPIの使い方を紹介する.

Media Browser Layout

Leanback support libraryにはカテゴリとメディアを少ないコードで実現するための主要レイアウトBrowseFragmentクラスを提供する.

どうやってBrowserFragmentでコンテンツをレイアウトするかの例を次ぎに記す.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  >

  <fragment
      android:name="android.support.v17.leanback.app.BrowseFragment"
      android:id="@+id/browse_fragment"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      />
</LinearLayout>

BrowserFragmentにレイアウトパラメータを設定するサンプルコードは下記.

public class BrowseMediaActivity extends Activity {
    public static final String TAG ="BrowseActivity";
    protected BrowseFragment mBrowseFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.browse_fragment);

        final FragmentManager fragmentManager = getFragmentManager();
        mBrowseFragment = (BrowseFragment) fragmentManager.findFragmentById(
                R.id.browse_fragment);

        // Set display parameters for the BrowseFragment
        mBrowseFragment.setHeadersState(BrowseFragment.HEADERS_ENABLED);
        mBrowseFragment.setTitle(getString(R.string.app_name));
        mBrowseFragment.setBadgeDrawable(getResources().getDrawable(R.drawable.ic_launcher));
        mBrowseFragment.setBrowseParams(params);

    }
}

Displaying Media Lists

BrowseFragmentはAdapterとPresenterでメディアコンテンツカテゴリから閲覧可能なカテゴリとメディア項目を表示できる.

次のコードはPresenterで文字データを表示するサンプル.

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

メディア項目を表示するためにpresenterを構築し,BrowseFragmentにAdapterをアタッチする.

カテゴリとメディア項目をStringPresenterを使って表示するためのAdapterを構築する方法を次に示す.

private ArrayObjectAdapter mRowsAdapter;
private static final int NUM_ROWS = 4;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    buildRowsAdapter();
}

private void buildRowsAdapter() {
    mRowsAdapter = 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, null);
        mRowsAdapter.add(new ListRow(header, listRowAdapter));
    }

    mBrowseFragment.setAdapter(mRowsAdapter);
}

このサンプルコードはAdapterに直接データを埋め込んでいるが,通常はオンラインデータベースやWebサービスからデータを取得する.

Web上のデータを利用してブラウジングするサンプルアプリケーションはAndroid TVを参照.

Updating the Background

TVのメディアブラウジングアプリケーションで興味を誘う視覚効果を追加するために,コンテンツの閲覧を通して背景画像を更新する.

このテクニックはアプリケーションとのインタラクションをよりシネマティックに,楽しいものにする.

Leanback support libraryはTVアプリケーションのActivityが背景を変更できるようにBackgroundManagerクラスを提供する.

次のコードは背景を更新する簡易な方法.

protected void updateBackground(Drawable drawable) {
    BackgroundManager.getInstance(this).setDrawable(drawable);
}

メディア一覧でのメディアブラウズアプリケーションはユーザ操作を通して自動で背景が更新される.

これを実現するには,Selection listenerを設定することで背景を更新する.

次はOnItemSelectedListenerクラスで選択イベントをキャッチし,背景を更新するサンプルコード.

protected void clearBackground() {
    BackgroundManager.getInstance(this).setDrawable(mDefaultBackground);
}

protected OnItemSelectedListener getDefaultItemSelectedListener() {
    return new OnItemSelectedListener() {
        @Override
        public void onItemSelected(Object item, Row row) {
            if (item instanceof Movie ) {
                URI uri = ((Movie)item).getBackdropURI();
                updateBackground(uri);
            } else {
                clearBackground();
            }
        }
    };
}

上記はあくまで一例である.

このファンクションを実装する場合,より良いパフォーマンスを得るためにバックグラウンドスレッドで更新すべき.

さらにアイテムをスクロールしながら閲覧させる場合,背景画像をアップデートするのにアイテムの選択から多少のディレイを加えること.

これにより背景画像の過度な更新を回避できる.

DetailsFragment

Leanback support libraryはメディアブラウジングのためのインタフェースクラスを提供する.

このクラスはメディアアイテムの説明やレビュー,そのアイテムに対してのアクション,購入,再生などの付加情報を表示する.

このセクションではメディア項目詳細のためのPresenterクラスの作り方,詳細画面のためのDetailsFragmentの拡張方法を論じる.

以降のサンプルコードではDetailsFragmentのためにActivityを作成しているが, BrowserFragmentとDetailsFragmentをFragmentトランザクションを使って置き換えることでActivityを1つで済ませることもできる.

Build a Details Presenter

メディアブラウジングフレームワークはLeanback support libraryで提供される.

詳細情報を含めたスクリーン上のデータ表示を制御するためにPresenterオブジェクトを使用できる.

メディア項目詳細のためのPresenterとしてフレームワークはほぼ完全な実装をAbstractDetailsDescriptionPresenterクラスとして提供する.

ViewにデータをバインドするためのonBindDescription()メソッドを実装すること.

次はそのサンプルコード.

public class DetailsDescriptionPresenter
        extends AbstractDetailsDescriptionPresenter {

    @Override
    protected void onBindDescription(ViewHolder viewHolder, Object itemData) {
        MyMediaItemDetails details = (MyMediaItemDetails) itemData;
        // In a production app, the itemData object contains the information
        // needed to display details for the media item:
        // viewHolder.getTitle().setText(details.getShortTitle());

        // Here we provide static data for testing purposes:
        viewHolder.getTitle().setText(itemData.toString());
        viewHolder.getSubtitle().setText("2014   Drama   TV-14");
        viewHolder.getBody().setText("Lorem ipsum dolor sit amet, consectetur "
                + "adipisicing elit, sed do eiusmod tempor incididunt ut labore "
                + " et dolore magna aliqua. Ut enim ad minim veniam, quis "
                + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
                + "commodo consequat.");
    }
}

Extends the Details Fragment

メディア項目詳細の表示にDetailsFragmentクラスを使用している場合,メディアアイテムへのアクションとプレビューを拡張できる.

また,関連するコンテンツリストとして追加でコンテンツを表示できる.

前章でのPresenterクラスの使い方についてサンプルコードを示す.

メディアアイテムが表示される際のプレビューとアクションの追加を行う.

また,このサンプルは関連するメディアコンテンツのリストも追加している.

public class MediaItemDetailsFragment extends DetailsFragment {
    private static final String TAG = "MediaItemDetailsFragment";
    private ArrayObjectAdapter mRowsAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "onCreate");
        super.onCreate(savedInstanceState);

        buildDetails();
    }

    private void buildDetails() {
        ClassPresenterSelector selector = new ClassPresenterSelector();
        // Attach your media item details presenter to the row presenter:
        DetailsOverviewRowPresenter rowPresenter =
            new DetailsOverviewRowPresenter(new DetailsDescriptionPresenter());

        selector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
        selector.addClassPresenter(ListRow.class,
                new ListRowPresenter());
        mRowsAdapter = new ArrayObjectAdapter(selector);

        Resources res = getActivity().getResources();
        DetailsOverviewRow detailsOverview = new DetailsOverviewRow(
                "Media Item Details");

        // Add images and action buttons to the details view
        detailsOverview.setImageDrawable(res.getDrawable(R.drawable.jelly_beans));
        detailsOverview.addAction(new Action(1, "Buy $9.99"));
            detailsOverview.addAction(new Action(2, "Rent $2.99"));
        mRowsAdapter.add(detailsOverview);

        // Add a Related items row
        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(0, "Related Items", null);
        mRowsAdapter.add(new ListRow(header, listRowAdapter));

        setAdapter(mRowsAdapter);
    }
}

Creating a Details Activity

DetailsFragmentなどのFragmentはActivityに含める必要がある.

ブラウズ用Activityと詳細表示用Activityを分ける場合,Intentを使って後者のActivityを呼び出すことができる.

このセクションでは詳細表示するためのActivityの実装方法について述べる.

まずは詳細表示用のActivityのためのDetailsFragmentを実装したレイアウトを作成する.

<!-- file: res/layout/details.xml -->

<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:name="com.example.android.mediabrowser.MediaItemDetailsFragment"
    android:id="@+id/details_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
/>

次にActivityを作成し,先ほどのレイアウトを設定する.

public class DetailsActivity extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.details);
    }
}

最後に作成したActivityをAndroidManifest.xmlに追加.この時,Leanbackテーマの適用を忘れないこと.

<application>
  ...

  <activity android:name=".DetailsActivity"
    android:exported="true"
    android:theme="@style/Theme.Leanback"/>

</application>

Listener for Clicked Items

DetailsFragmentを実装した後は,ユーザがメディアアイテムを選択した時にそこへ遷移させるように変更する.

これを実装するにはOnItemClickedListenerをBrowserFragmentに実装し,詳細表示ActivityのIntentを発行させる.

下記は閲覧用Activityでユーザがメディア項目をクリックした時に詳細表示用Activityに遷移させるリスナーを実装したコード.

public class BrowseMediaActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        // create the media item rows
        buildRowsAdapter();

        // add a listener for selected items
        mBrowseFragment.setOnItemClickedListener(
            new OnItemClickedListener() {
                @Override
                public void onItemClicked(Object item, Row row) {
                    System.out.println("Media Item clicked: " + item.toString());
                    Intent intent = new Intent(BrowseMediaActivity.this,
                            DetailsActivity.class);
                    // pass the item information
                    intent.getExtras().putLong("id", item.getId());
                    startActivity(intent);
                }
            });
    }
}

TVControllerにおけるAPIの利用例

MainActivity

単に「active_main」layoutを指定しているだけである。

「active_main」layoutでは、「MainFragment」を指定しているだけである。

従って、「MainFragment」が主役のようである。

MainFragment

単に「active_main」layoutを指定しているだけである。

「active_main」layoutでは、「MainFragment」を指定しているだけである。

従って、「MainFragment」が主役のようである。それもその筈、「MainFragment」は「BrowseFragment」を継承して作成されている。

メニューを定義する(loadRows)

メニューを定義する(loadRows)

ユーザ入力イベントの定義を行う(setupEventListeners)



蓮の花1