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に関する内容で自分で書いてみようと思っています。