はじめまして。AWA株式会社でAndroidアプリの開発を担当している奈良と申します。
AWA Androidアプリは2016年9月にAndroid TVへの対応をリリースしました。この記事では、AWAのAndroid TV対応に関して書かせていただきます。

12541_ext_10_0_l
2016年9月のバージョンアップでAndroid TVに対応しました。

Android TVとは

その名のとおりTV向けのAndroid OSで、2014年のGoogle I/Oで発表されました。Nexus Playerのように通常のテレビ(ディスプレイ)に接続するタイプと、SONY BRAVIAのようにAndroid TVを内蔵したテレビがあります。 ここでは主に「Android TVアプリを開発する」という観点での、モバイルとの違いを挙げます。

いくつかのハードウェア機能がサポートされていない

モバイル端末では当たり前にサポートしているタッチスクリーン、カメラ、GPS等の機能がサポートされていません。特にユーザーの操作がタッチではなくD-padによる操作なので、それを念頭において画面レイアウトを組んでいくことになります。

参考:Handle Unsupported Hardware Features

Webブラウザ、メールアプリがない

標準搭載されているアプリが少ないので、モバイルアプリで多用するIntent連携はほぼ使えません。よく使いそうなもので投げても大丈夫なのは、Google Play Store、YouTubeくらいになるでしょうか。

Notification → Recommendations

Android TVではNotificationはRecommendationsとしてユーザーに通知されます。Recommendationsについては別項で説明します。

minSdkVersion=21

これは開発者にとってはうれしい違いだと思います。Android TVはAndroid 5.0以上なので、5.0未満の端末を考慮する必要がありません。

アプリ公開前にGoogleによる審査がおこなわれる

アプリがAndroid TVで正常に動作するか、TV App Qualityに準拠しているかどうかがチェックされます。ここでのチェックはあくまでガイドラインに準拠しているかどうかなので、アプリの内容について厳しくチェックされるようなものではありません。

AWAモバイルアプリとの関係

Android TV対応をするにあたり、まず検討したのが「すでにリリースしているモバイルアプリにTV機能を追加するのか、それとも別アプリとして新規リリースするか?」でした。最終的に、モバイルアプリにTV機能を追加するかたちでストアにリリースしましたが、これが最適解だったかどうかは現時点でも確信は持てていません。おそらくどちらを選んでも同じような感覚を持ったと思います。

まず検討するにあたって、メリット・デメリットを洗い出しました。 モバイルアプリと同一apkにした場合、以下のようなことが挙げられます。

モバイルアプリの機能(Model)をそのまま使いまわせる

AWAのように既に機能が多く実装されている場合、既存の機能をそのまま使えるというのはやはり大きなメリットでした。Model部分がうまくモジュール化できていればまた違った選択肢があったと思いますが、スケジュール等も考慮して現実的な選択をしました。

モバイルアプリとTVアプリのバージョンに差異がなくなる

AWAはサービス開始後も頻繁に機能追加・変更や不具合修正を繰り返しており、日常的にバージョンアップを行っています。同一apkにしておけば強制的にリリースバージョンを揃えられるというのも同一アプリにした大きな理由でした。

プロジェクトが肥大化する

ここからデメリットです。モバイルとTVのコードがどちらも入ってしまうので、プロジェクトは大きくなりますし、構成も複雑になります。 この点については、パッケージやモジュールの分割を行うことで、ある程度は悪影響を抑えることができると考えました。

一部機能でマニフェストでのフィルタリングができなくなる

モバイルとTVで同一アプリとしてリリースする場合で、かつTVでサポートしていない機能をモバイル側で使っている場合、それらの機能についてはAndroidManifest.xmlで明示的に「必要としない」旨を宣言しなければいけません
マニフェストレベルで機能のチェックが行われないので、場合によってはコード内で「現在動作している端末で必要な機能がサポートされているかどうか」をチェックする必要が出てきます。

ざっとこのような項目を検討材料としました。モバイルと別アプリにする場合は、上記のメリット・デメリットを逆に見ていただければよいと思います。

AWAの場合、モバイルとTVの違いがほぼUIだけだったので「既存のAWAモバイルアプリに、Android TV上で起動された場合のUI一式を追加した」ようなイメージになっています。モバイルとTVではランチャーとなるActivityが別になっているので、お互いのUI部品にはまったく干渉しないようにし、Modelに関しては同じものを参照しています。

どのように実装したか

モバイルとTVで処理を分岐する

これはAWAのようにモバイルとTVで同一アプリとして開発する場合に関係してきますが、公式ドキュメントには以下の条件でTV判定が可能と書かれています。

UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
    // TV
} else {
    // non-TV
}

基本的にはこれでよさそうですが、TV以外でもUI_MODE_TYPE_TELEVISIONを返す端末もあったという記事もあり、上記に加えてAPI 21以降を加えたものをTV端末として判断するようにしました。

参考:最近Android TVアプリの開発で困ったこと

また、同一のリソースIDでモバイルとTVで内容を変更する場合もあるため、上記と同様の条件で内容を切り替えられるように、res/values-television-v21ディレクトリを作成しました。 最終的に、コード内でUiModelManagerを使用して判断する部分もなくし、以下のようなリソースでの判定に統一しました。

// res/values/bools.xml
<bool name="is_tv_device">false</bool>

// res/values-television-v21/bools.xml
<bool name="is_tv_device">true</bool>

ただし、アプリ内のコード・リソースから参照する場合はリソースの参照切り替えでいけるのですが、AndroidManifest.xmlから参照するリソースについては、television等の条件によって参照先を切り替えてくれないようなので、この点は注意しておいたほうがよいと思います。

参考:AndroidManifest.xml does not support configuration specific resources

v17 Leanback Library

AWAのAndroid TVアプリではLeanback Libraryを使用しています。 Android TVアプリで多用されるカードレイアウトや、フォーカス移動での操作に親和性の高いVerticalGridViewHorizontalGridView、基本的なレイアウトの画面を構築するためのベースとなるFragment等が提供されています。また、Leanbackのルールに準拠してUIを実装すれば、GoogleのAndroid TVアプリ公開審査基準を同時に満たすことになります。 google samplesにコードがありますので、基本的な実装についてはそちらを参考にしていただければと思います。

参考:androidtv-Leanback、 leanback-showcase

実際にLeanbackを使って開発をおこなった感想をいくつか書いていきます。

  1. Leanback標準に則った画面の開発は容易
  2. 反面、カスタマイズの自由度が低く、独自色を出そうとするとコストが高い
  3. 開発に関する情報が少ない

1、2についてはライブラリを使用する場合によくあることですが、Leanbackの場合は特にTVのUIに特化しているため、実装ルールがかなり厳しめに決められているという印象でした。もちろんLeanbackを使わずにTV用のUIを作ることも可能ですが、Leanbackを使いつつ特定の部分だけカスタマイズするというのはなかなか面倒な仕事でした。
カスタマイズするためにLeanbackのコードを読みましたが、カスタマイズしたい部分がパッケージ外から参照不可になっていることが多く、原則、ルール通りに作るのがよさそうです。

3については、TV開発をとっつきにくくしている原因のひとつになっていると思います。検索しても公式サンプル(と、公式をベースにした解説記事)以外がなかなかヒットせず、独自にカスタマイズしようとした場合はLeanbackのコードを読んでどうするか考える、というアプローチがほとんどでした。この記事で少しでもそのとっかかりになれればと思い、簡単な例ですが、以下にAWAで画面を構築した例を書いていきます。

Leanbackカスタマイズ例

AWAでLeanbackをカスタマイズしている例として、New ReleasesやNew Arrivalsのような「先頭行は横スクロールのRowアイテム、その下にGridを表示」している画面を挙げます。
Leanbackでは横スクロールのRowを並べて表示できるRowsFragment、Grid表示用のVerticalGridFragmentがそれぞれ提供されていますが、それらを組み合わせて表示できるようなコンポーネントは残念ながら提供されていません。

awa_tv_new_arrivals_1
New Arrivals画面の先頭行にはアーティストが横一列に表示されています。
awa_tv_new_arrivals_2
2行目以降はアルバムがGrid表示されています。

この画面はRowsFragmentをベースに実装しています。2行目以降のGrid表示に関しては、各Rowのアイテム数を固定することで擬似的にGrid表示をおこなっています。

RowsFragmentでは、水平方向のフォーカス位置については各Rowがそれぞれ個別のポジションを保持しているため、単純にアイテム数を揃えただけではフォーカス移動が不自然になってしまいます。そのため、擬似Grid表示している各Rowの水平方向のフォーカス位置を同期させる処理を割り込ませることで、見た目通りのフォーカス移動順を実現しています。

独自の画面を作る

もちろんLeanbackのFragmentを継承せず、独自に実装することも可能です。
LeanbackのFragmentを継承した場合はデフォルトでガイドラインに沿ったoverscan領域が設定されていますが、独自実装の場合はそのあたりも考慮してレイアウトを組む必要があります。

awa_tv_edit_playlist_tracks
プレイリスト編集のトラック編集画面。カーソル操作で行の入れ替えをおこなっています。

この画面はプレイリスト編集のトラック並べ替え・削除画面ですが、通常のFragmentを継承して画面を構築しています。
ベースにはVerticalGridViewを使用していますが、これはRecyclerViewのサブクラスなので、ここでは通常のRecyclerViewと同様に処理を実装しています。注意する点としては、フォーカス移動が複雑にならないようなレイアウトを心がけることで、この画面ではリストを垂直方向にフォーカス移動するので、リスト内の各アイテムでは水平方向のみフォーカス移動するようなレイアウトにしています。

また、モバイルアプリの同画面ではドラッグによる行の入れ替えがありますが、TVではドラッグするというユーザーアクションがないため、決定ボタン、カーソルボタンのクリックだけで同様の機能を提供しています。
具体的には以下のように、ドラッグ中のカーソル操作をハンドリングして実現しています。

// PlaylistTrackEditAdapter.java
holder.handleBtn.setOnClickListener(v -> {
    // ドラッグ状態を切り替える(ViewHolderにドラッグ状態を持たせている)
    holder.isDragged = !holder.isDragged;
    
    // ドラッグ状態に応じて表示を更新する
});

holder.handleBtn.setOnKeyListener((v, keyCode, event) -> {
    if (holder.isDragged) {
        
        if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
            if (event.getAction() == KeyEvent.ACTION_UP) {
                // 上方向に入れ替える
            }
            return true;
        }

        if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
            if (event.getAction() == KeyEvent.ACTION_UP) {
                // 下方向に入れ替える
            }
            return true;
        }
        
        // ドラッグ中は左右クリックは無視する
        return keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT;
    }
    return false;
});

フォーカスを意識した実装

フォーカス移動先の動的な変更

ユーザーがAndroid TVアプリを操作する際、タッチではなくカーソルキーによるフォーカス移動とクリックでおこないます。

Leanbackのコンポーネントをそのまま使う場合は特に意識しなくても問題はありませんが、独自の画面を構築する場合は自然なフォーカス制御を意識して実装する必要があります。例えば、Viewの位置を動的に変更するような画面の場合、状況に応じてフォーカス移動先を設定しなければ不自然になる場合があります。

AWAの画面で例を挙げると、メイン画面を独自実装しており、メニューの表示状態によってフォーカス移動のルールを切り替えています。

awa_tv_main_menu_focus_opened
メニュー表示時はメニュー(HeadersFragment)にフォーカスを移動する。
awa_tv_main_menu_focus_closed
メニュー非表示時はコンテンツ(ContentFragment)にフォーカスを移動する。

このメイン画面のレイアウト構成は大まかに以下のようになっています。

// tv_main_browse_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v17.leanback.widget.BrowseFrameLayout
    android:id="@+id/browseFrame"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:descendantFocusability="afterDescendants">

    <!-- Container for ContentFragment -->
    <FrameLayout
        android:id="@+id/contentContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginStart="270dp"/>

    <!-- Container for HeaderFragment -->
    <FrameLayout
        android:id="@+id/headersContainer"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:paddingEnd="50dp"/>

    <!-- android.support.v17.leanback.widget.TitleView -->
    <include layout="@layout/lb_browse_title"/>

</android.support.v17.leanback.widget.BrowseFrameLayout>

このレイアウトにあるBrowseFrameLayoutはLeanbackが提供しているクラスで、子View間のフォーカス移動ルールを設定できます。
以下のコードでは、検索ボタンから下方向にフォーカス移動する場合のルールを定義しています。

// MainBrowseFragment.java
mBrowseFrame.setOnFocusSearchListener((View focused, int direction) -> {
    // 検索ボタンから下方向へのフォーカス移動先を明示的に指定
    if (focused.getId() == android.support.v17.leanback.R.id.title_orb
            && direction == View.FOCUS_DOWN) {
        // メニュー表示中はメニュー、それ以外はコンテンツにフォーカス移動
        if (mIsHeaderMenuOpen) {
            return mHeadersFragment.getView();
        } else {
            return mContentFragment.getView();
        }
    }
    return null;
});

なお、上記のような画面(メニュー選択に応じてコンテンツを丸ごと入れ替える)は以前は独自実装するしか方法がありませんでしたが、Leanback Library v24.0.0で追加されたPageRowを使用することで標準的な実装でも実現可能となっていますので、これから開発する方にはそちらをお勧めします。

モーダル表示の注意点

もうひとつの例として、処理の実行中やエラー発生時のモーダル表示を挙げます。
APIの実行中、AWAでは全画面でFragmentを表示して処理中の表示を行なっています。

tv_main_loading
Viewを上からかぶせるだけではD-padのコントロールを抑止できません。

モバイルの場合はそれだけでユーザー操作を抑止できるのですが、TVの場合はカーソル移動により、裏側にあるFragmentに普通にフォーカスが移動してしまいます。 そのため、ユーザーに特定の操作しかさせたくない(特定のViewしかクリックさせたくない)場合は、対象となるViewにフォーカスを移動させると同時に、そこからフォーカスを移動できなくする制御が必要です。
処理中表示Fragmentは以下のように実装しています。

// res/layout/tv_pending_fragment.xml
<!-- どの方向に移動しても自身にフォーカスが戻る -->
<ImageView
    android:id="@+id/logo"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:nextFocusDown="@id/logo"
    android:nextFocusLeft="@id/logo"
    android:nextFocusRight="@id/logo"
    android:nextFocusUp="@id/logo"
    android:src="@drawable/logo"/>
// PendingFragment.java
private ImageView mLogo;

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.tv_pending_fragment, container, false);
    mLogo = (ImageView) view.findViewById(R.id.logo);
    return view;
}

@Override
public void onResume() {
    super.onResume();
    // 表示中はロゴにフォーカスを与える
    mLogo.requestFocus();
}

Leanbackで提供されているErrorFragmentも同様の実装となっており、常にボタンにフォーカスが当たった状態になっています。

Recommendations

Recommendationsはホーム画面の上段、もっとも目立つところにカードを表示する機能です。
アプリケーション側でNotificationを作成すると、TVの場合はRecommendationsとしてホーム画面に表示され、ユーザーはホーム画面から直接、興味のあるコンテンツにアクセスすることが可能になります。

awa_tv_recommendation
ホーム画面の先頭行に表示されます。

Notificationを通知したタイミングでRecommendationsは作成されるので、更新タイミングはアプリケーション側でハンドリング可能です。AWAでは、バックグラウンドで定期的に更新タスクを起動させて、その時点で最新のプレイリスト情報の中から、特にユーザーにお勧めしたいものを数点、Recommendationsとして表示しています。

Recommendationsフォーカス時の背景変更

Recommendationsの基本的な実装は公式ドキュメントとサンプルに書かれていますが「Recommendationsカードにフォーカスされたときのホーム背景変更」についての情報がなかなか見つかりませんでした。
以下の記事が具体的な実装方法も含めて書かれており、大変参考になりました。ありがとうございます。

参考:[Android TV] recommendation の背景background imageについて

Global Search

Global Searchはホーム画面最上段にある検索に対して、各アプリケーションが直接検索結果を返すことができる機能です。こちらもRecommendationsと同じく、少ない操作でユーザーが目的のコンテンツにたどり着くための機能になっています。

awa_tv_global_search
ホーム画面での検索に対して結果を返すことができます。

こちらは、問い合わせに対して検索結果を返すContentProviderを実装することで機能を実現します。
Recommendationsとは違い、ユーザー操作に対して応答を返す機能なので、レスポンスを考慮して実装を進めたほうがよいと思います。また、検索結果のカードレイアウトで表示する画像のサイズが270×400、400×225の2種類しかないため、規定のサイズで表示されても不自然にならないように注意したほうがよいでしょう。

Android TVが不得意なことを理解する

Android TVはモバイルとはUIが大きく異なるため、モバイルアプリと同じ感覚で実装するとユーザーに不便を強いる場合も出てきます。実装時に注意したほうがよい点、AWAでどのように対応したかをいくつか挙げていきます。

細かな情報の表示

Android TVアプリには「Simple. Cinematic. Beautiful.」であることが求められます。大画面をいかした美しい画像や動画の表示、少ない操作で目的のコンテンツにたどり着けるシンプルなインターフェースを実現することで、モバイルアプリとは違ったユーザー体験を提供できます。
その反面、細かい情報を表示するのが苦手なので、長い文章を画面いっぱいに表示するようなことは避けるべきです。なお、文字サイズについては12sp以上、メインとなる文字については16sp以上を確保しておかないとAndroid TVのガイドラインに違反するので、公開前審査を通過できません。

文字入力

これは実際にD-padで文字入力をしてみるとわかりますが、Android TVでの文字入力はユーザーに多大な負荷をかけるので、可能な限り避けたほうがよいでしょう。

AWAではAndroid TVアプリだけでもStandardプランの定期購読が可能になっています。そのため、Android TVアプリ単体でアカウントの登録、ログイン可能である必要がありますが、D-padでの文字入力でメールアドレスやパスワードの入力を強いるのはユーザーの快適さを大きく損なうことは明白でした。
その解決策として、モバイルアプリと連動したQRコードによるログインをメインの導線として配置しました。モバイル端末をお持ちであれば、Android TVアプリの画面に表示されたQRコードをAWAのモバイルアプリで読み取ることで、モバイルとTVのアカウントを同期させることができます。

awa_tv_activation
メールアドレスでのログインも可能ですが、より簡単なQRコードでのログインを目立たせています。

ここにも「ユーザーにストレスをかけさせないUIを提供したい」というAWAの思想が反映されています。

複雑な画面遷移が必要な機能

Android TVアプリにとって、ユーザーが直感的に操作してコンテンツを楽しめることは重要な項目なので、複雑な画面遷移をともなう機能とは相性がよくありません。

AWAのプレイリスト作成では「タイトル、説明文の編集」「ムード選択」「トラックの検索・追加」「並べ替え・削除」といった機能が組み合わさっています。Android TVアプリでは、ここをできるだけ簡略化するため「トラックの検索・追加」を思い切ってプレイリスト作成機能から外し、プレイリストへのトラック追加は、トラックメニューから行う導線に一本化しました。 Android TVアプリは編集関連の機能ではどうしても操作性が劣ってしまうので、どんどんプレイリストを作成したいユーザーには、モバイル・PCアプリで作成してもらうことを想定しています。
この考えは、AWAが幅広いデバイスに対応できているからこそ選択できるものでした。

Intent連携

上でも触れましたが、モバイルアプリで当たり前のようにおこなわれているIntent連携については、TVではほぼ使えないと考えたほうがよいと思います。

AWAのモバイルアプリでは利用規約等を表示するためにIntent連携でWebブラウザを表示していましたが、Android TVアプリではWebViewでの実装に変更しました。なお、アプリ内でWebViewを表示する場合、WebView内のコンテンツもD-padで操作可能である必要があります。
お問い合わせで必要なメール送信については、QRコードによるお問い合わせメールテンプレートをおこないました。併せて、QRコードを利用できないユーザー向けに、お問い合わせメールアドレスの表示もおこなっています。

さいごに

いかがだったでしょうか。Android TVに関しては現時点で対応しているサービスも多くなく、情報もあまり出回ってないため、今回は概要とちょっとしたTipsのような構成で書かせていただきました。UIについても「こうすればOK」と言えるようなものも確立されておらず、その分、自分のアイデアで新しいUIを提案できる楽しみもあると思います。

最近はWearのほうが盛り上がってる感もありますが、TVも負けずにいろんなアプリが世に出て盛り上がってくれることを期待しております。実際に開発してみて、やはりよくわからないものをいろいろ調べながら実装進めるのは楽しいなあ、ということを再確認できた数ヶ月でした。この記事で少しでも興味を持ってくれた方がいらっしゃれば幸いです。

最後まで読んでいただきありがとうございました。

2011年12月中途入社、2015年4月よりAWAのAndroidチームに所属。 天神にあるAWA福岡支社で勤務しています。最近お医者さんからビールと日本酒を控えるように言われました。