道産子エンジニア

毎週好きなこと書きます。

【AndroidTV】android.support.v17.leanbackの中を読む ~part1. FocusHighlight編~

CyberAgentAdventCalendar4日目を担当するまえかわです。 前回は山本孔明さんのAnsible2.4でネットワークプログラマビリティな運用を考えるでした。

--

現在はAbemaTVで開発を行なっており、AndroidTVデバイスの開発を行なって一ヶ月ほど経ちました。AndroidTVの開発では基本的にandroid.support.v17.leanbackのコンポーネントを使用していくことになります。テレビ用のアプリを作るにはleanbackライブラリのことを理解するのが重要です。今回はleanbackの中でも重要なpackage内のクラスをざっと紹介し、さらにドキュメントでは詳しく紹介されてないFocusHighlightに関するAPIの内部実装を読んだので紹介します。

ここではAndroidTVにおけるUIデザインの思想やデザインガイドについては紹介しません。また、公式のドキュメントに書いてあることを詳しくは載せません。日本語ドキュメントなどコンポーネントの紹介記事は充実しているので、詳しく紹介していない部分については他の記事を参照してください。

leanback概要

まずは重要なコンポーネント群について簡単に紹介します。 f:id:yuichi31:20171203224048p:plain この図は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を使った画面の組み方なども見えてきます。また、ProgressBarManagerBackgroundManagerといったコンテンツ表示に関する共通処理を用意していてくれているのもleanbackならではです。

PlaybackFragmentVideoFragmentは動画再生に使うものですが、再生のコントローラーをGlueと呼び以下のような種類があります。

https://developer.android.com/training/tv/images/glue_side_by_side.png

Presenter

画面関連のクラスの全体像がわかりましたが、他にも今回紹介する内容の理解に必要なPresenterを紹介しておきます。

leanbackでは簡単に言うとAndroidアプリにおけるRecyclerviewGridview(Horizontal or Vertical)として扱い、AdapterPresenterとして扱います。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の設定をVerticalGridPresenterListRowPresenterのコンストラクタで指定します。

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

focusZoomFactoruseFocusDimmerを指定しない場合は、それぞれ次の値がデフォルトとなります。

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

このとき渡しているmFocusZoomFactoermUseFocusDimmerでZoomとDimを調整します。これらを各Presenterの フォーカスが当たった時にZoomとDimといった機能でコンテンツを強調表示しています。ここで先ほど紹介したItemBridgeAdapterが登場します。

Zoomについて

Zoomサイズの調整

android.support.v17.leanback.widget パッケージ内にあるFocusHighlightというインターフェース内にZoomFactorが定義されており、カードアイテムのZoomサイズを調整できます。 それぞれに対応するzoomサイズを画像にしました。

focus size image
ZOOM_FACTOR_LARGE(118%) f:id:yuichi31:20171203144738p:plain:w300
ZOOM_FACTOR_MEDIUM(114%) f:id:yuichi31:20171203144751p:plain:w300
ZOOM_FACTOR_SMALL(110%) f:id:yuichi31:20171203144806p:plain:w300
ZOOM_FACTOR_XSMALL(106%) f:id:yuichi31:20171203144829p:plain:w300
ZOOM_FACTOR_NONE f:id:yuichi31:20171203144852p:plain:w300

かなり細かいサイズの違いなのでわかりにくいですが、カード内の情報によってはかぶりすぎてしまわないように細かい調整が可能になっています。

FocusHighlightHelper内での実装

Presenterで呼び出されていた FocusHighlightHelper#setupBrowseItemFocusHighlight はこのようになっています。

public static void setupBrowseItemFocusHighlight(ItemBridgeAdapter adapter, int zoomIndex, boolean useDimmer) {
        adapter.setFocusHighlight(new BrowseItemFocusHighlight(zoomIndex, useDimmer));
}

BrowseItemFocusHighlightonItemFocusedを呼び出すインターフェースを実装しており、ItemBridgeAdapter#onFocusで呼び出され、FocusHighlightHelperのインナークラスFocusAnimatorsetFocusLevelを呼び出すことでアニメーションを行なっています。

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 f:id:yuichi31:20171204125823p:plain:w400
false f:id:yuichi31:20171204125841p:plain:w400

ColorOverlayDimmerのコンストラクタには三つの引数があります。

  • dimColor
  • activeLevel
  • dimmedLevel

dimColorはオーバーレイする色のことで、activeとdimmedLevelはそれぞれViewがactiveかどうでないかの状態を渡しているものです。

Zoomの時に紹介したFocusAnimatorsetFocusLevelで同じように呼び出されている mDimmerColorOverlayDimmerであり、 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に関する内容で自分で書いてみようと思っています。

フィリップKディック「ザップ・ガン」を読んだ

ブレードランナー2049の余韻も少し落ち着いてきたこの頃。

先々週くらいにアマゾンで激アツの海外SF小説セールが行われていたので、すかさずディック作品を買い漁った。

ここからはしばらくディックばかり読むことになると思う。

huyukiitoichi.hatenadiary.jp

ザップ・ガンはディック作品の中でもあまり人気のない?と前評判を聞いていたが、そんなことはなく自分は楽しめた。

だがディック本人もこの作品をクソだとインタビューに答えているようで、前半部分はまるで読めたものじゃないとまで言っていたらしい。

まだ生きていてTwitterアカウントでもあったら、「そんなことはない、これは最高だよ」ってメンションしたかった。

これを書いた頃のディックは、質より量的な執筆をしていた頃らしく、年に長編小説を6冊も出版していたそうだ。

そんな活動を見習いたい。失敗してもいい、駄作でもいいからとにかく作りたいって時はあると思う。そういう苦悩の末に何か見えればいいし、見えなくても後悔はないだろう。

ディックが読めたものじゃないという冒頭から、「自動TVレポーター」「兵器ファッションデザイナー」「コンコモディー」「コナプト」みたいなディックワールド全開な感じは本当に好きだ。 これはエヴァンゲリオンを作る庵野監督も似たような技法(?)を使っていて、初めて見る人にとっては全く何を言っているのかわからないところがいい。しかもこれらは綿密に設定された奥の深いストーリーというより、物語にとってはある程度意味がわかればいい部分であって、深追いする必要のない部分というのがいい。(この時点で何を言っているのかわからないと思うけど)オーヴィルくんみたいな印象的なマシンも登場するのが油断できない。

つまり何度も読み返したくなるし、別な作品も読んでみたくなるし、とにかくググッとまさにフィクションの世界に引き込まれるのだ。小説にもよるが延々と世界観の説明をされるのは想像力を掻き立てられないから好きじゃない。カオスでディックらしい舞台がすごくいい。

それとは別にディックらしい小説には特徴がある。よく「現実とSFの世界の境目がわからなくなってる」というニュアンスに近い書評や感想を見かけるが、俺の解釈では「自分が信じているものは見方によってはすぐに変わってしまう」というようなことだと感じている。そこをディックは原点にして作品を描いているように感じる。その対象が現実の世界と機械の世界であったり、現実と夢、人間とアンドロイドなど様々なものが当てはまる。そしてその対象は自分たちが思っているほど違うものであるのか?というのを小説を通じて投げかけられているように感じる。

ディック作品を読んでみるとすぐにわかってもらえると思う。映画マトリックスでネオが現実世界に気が付いたときくらいの衝撃を受けるだろう。

ようやくこのザップ・ガンについての感想を書くわけだけれども、皮肉にもディック自身がよく用いる表現の「蓼食う虫も好き好き」ということわざがこの作品には似合うかもしれない。この作品には作家としてのディックの苦悩そのものが描かれていて、主人公が見せかけの世界で自分がやるべきこと(しかないこと)を淡々とこなしていく生活と、このままではいけないという不安が物語を暗く、深くしていく重要な要素だった。人々を殺しもすれば救いもする「ザップ・ガン」はどんな兵器であるか想像するもよし、生物を脅かす本当の兵器とはなんなのか?そういった問いを考えさせられるいい作品だった。

ザップ・ガン

ザップ・ガン


次はシュミラクラ。

読書所用時間:約6時間
オススメ度:★★★☆☆