CyberAgentAdventCalendar4日目を担当するまえかわです。 前回は山本孔明さんのAnsible2.4でネットワークプログラマビリティな運用を考えるでした。
--
現在はAbemaTVで開発を行なっており、AndroidTVデバイスの開発を行なって一ヶ月ほど経ちました。AndroidTVの開発では基本的にandroid.support.v17.leanbackのコンポーネントを使用していくことになります。テレビ用のアプリを作るにはleanbackライブラリのことを理解するのが重要です。今回はleanbackの中でも重要なpackage内のクラスをざっと紹介し、さらにドキュメントでは詳しく紹介されてないFocusHighlightに関するAPIの内部実装を読んだので紹介します。
ここではAndroidTVにおけるUIデザインの思想やデザインガイドについては紹介しません。また、公式のドキュメントに書いてあることを詳しくは載せません。日本語ドキュメントなどコンポーネントの紹介記事は充実しているので、詳しく紹介していない部分については他の記事を参照してください。
leanback概要
まずは重要なコンポーネント群について簡単に紹介します。
この図はAndroidTVでの開発を行うとき、何度も参照することになるandroidtv-leanback | Google Samples on GithubのREADMEから持ってきました。AndroidTVらしい画面を作るのに欠かせないこれらのコンポーネントは、多くの他の記事などでもよく解説されているためここでは詳しく紹介しません。一部を除くこれらのコンポーネントが含まれたパッケージが、android.support.v17.leanback.appです。継承関係などを見ていきましょう。
android.support.v17.leanback.app内のクラス
Fragment ├─BackgroundFragment ├─BrandedFragment(BrandedSupportFragment) │ ├─ErrorFragment(ErrorSupportFragment) │ └─BaseFragment(BaseSupportFragment) │ ├─BrowseFragment(BrowseSupportFragment) │ ├─DetailsFragment(DetailsFragmentBackgroundController) │ └─VerticalGridFragment(VerticalGridSupportFragment) ├─BaseRowFragment(BaseRowSupportFragment) │ ├─HeadersFragment(HeadersSupportFragment) │ └─RowsFragment(RowsSupportFragment) ├─OnboardingFragment(OnboardingSupportFragment) ├─GuidedStepFragment(GuidedStepSupportFragment) ├─PlaybackFragment(PlaybackSupportFragment) │ └─VideoFragment(VideoSupportFragment) └─SearchFragment(SearchSupportFragment) - BackgroundManager - FragmentUtil - ListRowDataAdapter - ProgressBarManager - PermissionHelper - PlaybackFragmentGlueHost - VideoFragmentGlueHost
たくさんのコンポーネントがあるので親になるクラスから読み解いていくのがオススメです。android.support.v17.leanback.appの中を確認することで、HeaderとRowを使った画面の組み方なども見えてきます。また、ProgressBarManagerやBackgroundManagerといったコンテンツ表示に関する共通処理を用意していてくれているのもleanbackならではです。
PlaybackFragmentやVideoFragmentは動画再生に使うものですが、再生のコントローラーをGlueと呼び以下のような種類があります。
Presenter
画面関連のクラスの全体像がわかりましたが、他にも今回紹介する内容の理解に必要なPresenterを紹介しておきます。
leanbackでは簡単に言うとAndroidアプリにおけるRecyclerviewをGridview(Horizontal or Vertical)として扱い、AdapterをPresenterとして扱います。MVPの思想でAPIを設計していて、そこからPresenterという名前が来ています。Presenterに関する詳しい解説は、同じくAbemaチームのogaclejapanさんのDroidKaigi2017での資料: your app nameで詳しく紹介されているので参照してください。
Presenterは今回の話以外の細かい部分を省くと以下の二種類が重要になります。
- 縦方向のGridに対応する
VerticalGridPresenter - 横方向のRowに対応する
ListRowPresenter
この二つのクラスが重要なZoomとDim機能をもっているので、次節で紹介します。
Presenterには紹介したい内容がたくさんあるのですが、今回は絞って書いています。個人的にはRecyclerViewのアダプタに対して、ObjectAdapter内のアイテムを受け渡している ItemBridgeAdapterクラスを読むのがおすすめです。ItemBridgeAdapterはこのあとも登場します。ItemBridgeAdapterのコメントには
Public to allow use by third party Presenters.
と記述されているので、Presenterを自作する際にも使えます。
ZoomとDim = FocusHighlight
他のAndroidTVに関する発表や記事でも紹介されていますが、leanbackの特徴的な動きにフォーカス制御が挙げられます。
テレビとリモコンつかうAndroidTVでは、コンテンツを 「より画面の中央に見やすく表示する」 ことを強く意識して設計されているようです。
そのためには選択したコンテンツを強調する必要があります。それらを実現するためにleanbackでは
- focusしているカードを大きく表示する
- focusしていないカードを目立たなくする
というアプローチを取っており、これらを合わせて FocusHighlight と呼び、それぞれZoomとDimという名前で実装されています。
FocusHighlightHelper
FocusHighlightを各カードに対して実行するのがFocusHighlightHelperであり、ZoomとDimの設定をVerticalGridPresenterとListRowPresenterのコンストラクタで指定します。
- ListRowPresenter
public ListRowPresenter(int focusZoomFactor, boolean useFocusDimmer) { if (!FocusHighlightHelper.isValidZoomIndex(focusZoomFactor)) { throw new IllegalArgumentException("Unhandled zoom factor"); } mFocusZoomFactor = focusZoomFactor; mUseFocusDimmer = useFocusDimmer; }
- VerticalGridPresenter
public VerticalGridPresenter(int focusZoomFactor, boolean useFocusDimmer) { mFocusZoomFactor = focusZoomFactor; mUseFocusDimmer = useFocusDimmer; }
focusZoomFactorとuseFocusDimmerを指定しない場合は、それぞれ次の値がデフォルトとなります。
| focusZoomFactor | useFocusDimmer | |
|---|---|---|
| ListRowPresenter | FocusHighlight.ZOOM_FACTOR_MEDIUM | false |
| VerticalGridPresenter | FocusHighlight.ZOOM_FACTOR_LARGE | true |
この設定をFocusHighlightHelperのイニシャライズ時に指定しています。
- ListRowPresenter
FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter,
mFocusZoomFactor, mUseFocusDimmer);
- VerticalGridPresenter
FocusHighlightHelper.setupBrowseItemFocusHighlight(vh.mItemBridgeAdapter,
mFocusZoomFactor, mUseFocusDimmer);
このとき渡しているmFocusZoomFactoerとmUseFocusDimmerでZoomとDimを調整します。これらを各Presenterの
フォーカスが当たった時にZoomとDimといった機能でコンテンツを強調表示しています。ここで先ほど紹介したItemBridgeAdapterが登場します。
Zoomについて
Zoomサイズの調整
android.support.v17.leanback.widget パッケージ内にあるFocusHighlightというインターフェース内にZoomFactorが定義されており、カードアイテムのZoomサイズを調整できます。
それぞれに対応するzoomサイズを画像にしました。
| focus size | image |
|---|---|
| ZOOM_FACTOR_LARGE(118%) | ![]() |
| ZOOM_FACTOR_MEDIUM(114%) | ![]() |
| ZOOM_FACTOR_SMALL(110%) | ![]() |
| ZOOM_FACTOR_XSMALL(106%) | ![]() |
| ZOOM_FACTOR_NONE | ![]() |
かなり細かいサイズの違いなのでわかりにくいですが、カード内の情報によってはかぶりすぎてしまわないように細かい調整が可能になっています。
FocusHighlightHelper内での実装
Presenterで呼び出されていた FocusHighlightHelper#setupBrowseItemFocusHighlight はこのようになっています。
public static void setupBrowseItemFocusHighlight(ItemBridgeAdapter adapter, int zoomIndex, boolean useDimmer) { adapter.setFocusHighlight(new BrowseItemFocusHighlight(zoomIndex, useDimmer)); }
BrowseItemFocusHighlightはonItemFocusedを呼び出すインターフェースを実装しており、ItemBridgeAdapter#onFocusで呼び出され、FocusHighlightHelperのインナークラスFocusAnimatorのsetFocusLevelを呼び出すことでアニメーションを行なっています。
void setFocusLevel(float level) { mFocusLevel = level; float scale = 1f + mScaleDiff * level; mView.setScaleX(scale); mView.setScaleY(scale); if (mWrapper != null) { mWrapper.setShadowFocusLevel(level); } else { ShadowOverlayHelper.setNoneWrapperShadowFocusLevel(mView, level); } if (mDimmer != null) { mDimmer.setActiveLevel(level); int color = mDimmer.getPaint().getColor(); if (mWrapper != null) { mWrapper.setOverlayColor(color); } else { ShadowOverlayHelper.setNoneWrapperOverlayColor(mView, color); } } }
Dimについて
Dimとは言葉の通りカードを暗くする機能のことであり、こちらもZoomと同じくFocusHighlightHelperがもつColorOverlayDimmerというクラスが行なっています。
Presenterのコンストラクタに渡したmUseFocusDimmerのフラグによってスイッチング可能です。(Zoomをnoneにしています)
| mUseFocusDimmer | image |
|---|---|
| true | ![]() |
| false | ![]() |
ColorOverlayDimmerのコンストラクタには三つの引数があります。
- dimColor
- activeLevel
- dimmedLevel
dimColorはオーバーレイする色のことで、activeとdimmedLevelはそれぞれViewがactiveかどうでないかの状態を渡しているものです。
Zoomの時に紹介したFocusAnimatorのsetFocusLevelで同じように呼び出されている mDimmerが ColorOverlayDimmerであり、 setActiveLevelの実装は以下です。
- ColorOverlayDimmer
/** * Sets the active level of the dimmer. Updates the alpha value based on the * level. * * @param level A float between 0 (fully dim) and 1 (fully active). */ public void setActiveLevel(float level) { mAlphaFloat = (mDimmedLevel + level * (mActiveLevel - mDimmedLevel)); mAlpha = (int) (255 * mAlphaFloat); mPaint.setAlpha(mAlpha); }
スクロールのアニメーションに合わせて、active <-> dimのレベルを渡しdimColorの濃淡を表現しているようです。
以上です。FocusHighlightの仕組みがわかってきたので、これを応用して自作のUIを作っていけそうです。
今回は長くなるので紹介しなかったwidgetパッケージについても別な記事で紹介する予定です。また、来年はDroidKiagi2018でAndroidTVに関する内容で発表を行います。
余裕があれば技術書典にAndroidTVに関する内容で自分で書いてみようと思っています。







