読者です 読者をやめる 読者になる 読者になる

inFablic

Fablic開発者ブログ

Android版フリルでの商品画面リニューアルにおけるCollapsingToolbarLayoutを用いたレイアウト構築

Android

この記事はFablic Advent Calendar 22日目の記事です。 http://qiita.com/advent-calendar/2016/fablic

こんにちは。Androidエンジニアの @nakamuuu です。

フリルは2016年10月、ロゴやアイコン、アプリ全体のカラーリングの変更を含む大規模なリニューアルを行いました。

Android版フリルではリニューアルに合わせ、商品画面の改修もしています。今回はこの商品画面でのCollapsingToolbarLayoutを用いたレイアウト構築、特に各Viewに指定されている fitsSystemWindows のパラメータの役割について実装時に少し掘り下げて調べてみたので書いていきたいと思います。

f:id:nakamuuu-muuu:20161222111315j:plain

この記事の概要

  • リニューアル後の商品画面のレイアウト構造
  • CollapsingToolbarLayoutを用いたレイアウト構築において浮かんだ疑問
    • fitsSystemWindows について改めて理解する
    • CollapsingToolbarLayoutなどに指定された android:fitsSystemWindows=true のそれぞれの役割

リニューアル後の商品画面のレイアウト構造

新しい商品画面ではAndroid Design Support Libraryに含まれるCollapsingToolbarLayoutを用いて、Material design guidelinesの Scrolling techniques にもあるようなスクロール位置に応じてアクションバーの表示が切り替わるような実装をしています。

f:id:nakamuuu-muuu:20161223014136g:plain:w360

以下のようにレイアウトの構成自体はAndroid Studioにテンプレートとして用意されている「Scrolling Activity」で自動生成されるものに少し手を入れたような簡単なものになっています。フリルの場合はスクロールに合わせてアイコンの色などを動的にJavaコード側から変更したりしていますが、基本的にはレイアウトさえ組んでしまえばJavaコード側で特殊な処理を行わなくても正しく動作するはずです。

(※実際の商品画面のレイアウトファイルから一部の要素を省略して掲載しています。)

<android.support.design.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:statusBarScrim="@color/status_bar"
            app:titleEnabled="false">

            <!-- 商品画像用のViewPager-->
            <ViewPager
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:fitsSystemWindows="true"
                app:layout_collapseMode="parallax" />

            <android.support.v7.widget.Toolbar
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />

        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <!-- (省略)商品情報や出品者情報のレイアウト -->

    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

CoordinatorLayoutの子ViewとしてAppBarLayoutとNestedScrollView(RecyclerViewなどのNestedScrollingを実装した他のViewGroupでも可)、さらにAppBarLayoutの中にToolbarと画像を表示するためのViewを内包するCollapsingToolbarLayoutを入れているというのが大まかな構造です。

CollapsingToolbarLayoutを用いたレイアウト構築において浮かんだ疑問

繰り返しになりますが、今回のリニューアル後の商品画面のようなレイアウトは基本的にはAndroid Studioのテンプレートとして用意されているものに大きく手を加えることなく実装することができます。しかし、そのレイアウトファイルを読んでいる中で2つの点で疑問が浮かびました。

  • CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout、ViewPagerで指定されている4つの android:fitsSystemWindows="true" がそれぞれどういった役割を果たしているのか
  • style定義やActivity内などで明示的に指定していないにも関わらずステータスバーがなぜ透過になるのか(そのような処理がどのViewで行われているのか)

いずれも把握していなくても商品画面の実装においては困ることがなさそうな細かい部分ですが、Android Design Support Libraryの各Viewのソースを追って軽く調査してみました。

そもそも fitsSystemWindows って何?

ステータスバーやナビゲーションバーを透過にする際によく使われる android:fitsSystemWindows 。個人的には今まで「ViewにシステムUI分のpaddingが付くようなもの」という何となくの解釈で使用していました。リファレンスでも以下のような説明になっていて、同じような解釈で使用している人もいるのではないでしょうか。

Boolean internal attribute to adjust view layout based on system windows such as the status bar. If true, adjusts the padding of this view to leave space for the system windows.

View | Android Developers

しかし、この解釈がすべてのViewにおいて正しいものとすれば、商品画面のレイアウトに4つも android:fitsSystemWindows="true" が付いていることが説明できなくなります。CoordinatorLayoutにシステムUI分のpaddingが付いてしまえば商品画像がステータスバー部分に表示されなくなる上、2つ目以降の fitsSystemWindows の指定は意味がないのでは *1 ということになってしまうためです。

実は各Viewは自身やまたその子Viewに fitsSystemWindows が指定されている場合、それぞれ独自の振る舞いをしています。これが4つの android:fitsSystemWindows="true" の役割を理解していく上で押さえておきたい重要なポイントです。

4つの android:fitsSystemWindows="true" それぞれの役割

fitsSystemWindows の挙動について少しおさらいした上で、各Viewの挙動を見ていきます。

① CoordinatorLayoutの android:fitsSystemWindows="true"

CoordinatorLayoutはAndroid 5.0以降かつ fitsSystemWindowstrue の場合に自身のコンストラクタの中で View#setSystemUiVisibility を呼び出してステータスバーの領域までレイアウトを配置できるようにしています。

setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);

SYSTEM_UI_FLAG_LAYOUT_STABLE はインセットの変化が連続的に反映されて負荷とならないように他のフラグと合わせて付与することが推奨されているもので、 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN はステータスバーが表示されていないかのようにレイアウトを配置したい場合に付与するパラメータ *2 です。

同様の処理はDrawerLayoutでも行われていて、fitsSystemWindowstrue になっているドロワー部分のViewをステータスバーの領域にまで表示できるようにしています。

CoordinatorLayoutでは他にも子ViewのBehavior *3 にシステムUI分のインセットを渡す処理なども行っていますが、AppBarLayout.Behaviorを見た限り、今回のレイアウトでは影響のない部分のようでした。

② AppBarLayoutの android:fitsSystemWindows="true"

AppBarLayoutに付いている android:fitsSystemWindows="true" の役割は単純です。渡ってきたシステムUI分のインセットをアクションバーが完全に表示されるまでに必要なスクロール量の計算において考慮するかどうかの切り分けに使われていました。

③ CollapsingToolbarLayoutの android:fitsSystemWindows="true"

CollapsingToolbarLayoutでは onAttachedToWindow の段階で親ViewがAppBarLayoutの場合に自身の fitsSystemWindows をAppBarLayoutに設定されている値に合わせています。そのため android:fitsSystemWindows="true" の指定は必須というわけではありません。ただ、挙動を把握していないと省略されていることがわかりにくいためAndroid Studioのテンプレートなどでも明示的に指定されているものと思われます。

役割としてはAppBarLayoutと同様に渡ってきたシステムUI分のインセットを保持して、スクロール時にそのインセットの上端の領域(=ステータスバーの領域)を app:statusBarScrim で指定された色(デフォルトでは colorPrimaryDark の値)で塗りつぶしているほか、④で後述する子Viewのレイアウト時に使用していました。

④ ViewPager(CollapsingToolbarLayoutの子View)の android:fitsSystemWindows="true"

CollapsingToolbarLayoutの子ViewとなるViewPagerに付いている android:fitsSystemWindows="true" の役割もCollapsingToolbarLayoutを見ることでわかります。CollapsingToolbarLayout#onLayout の前半部分を抜粋してみます。

if (mLastInsets != null) {
    // Shift down any views which are not set to fit system windows
    final int insetTop = mLastInsets.getSystemWindowInsetTop();
    for (int i = 0, z = getChildCount(); i < z; i++) {
        final View child = getChildAt(i);
        if (!ViewCompat.getFitsSystemWindows(child)) {
            if (child.getTop() < insetTop) {
                // If the child isn't set to fit system windows but is drawing within
                // the inset offset it down
                ViewCompat.offsetTopAndBottom(child, insetTop);
            }
        }
    }
}

Shift down any views which are not set to fit system windows というコメントの通りではありますが、 fitsSystemWindowsfalse なViewをシステムUI分(= insetTop )だけ下にずらしてレイアウトするという処理を行っています。

試しにフリルの商品画面のViewPagerから android:fitsSystemWindows="true" の指定を外してみると商品画像がステータスバー部分にまで表示されなくなってしまいました。ステータスバーの領域にまで表示したいCollapsingToolbarLayoutの子Viewには android:fitsSystemWindows="true" を指定、Toolbarのようにステータスバーには被らないで欲しい子Viewには指定しないように注意しましょう。

f:id:nakamuuu-muuu:20161221193148p:plain:w360

終わりに

今回の商品画面のリニューアルではAndroid Design Support Libraryで用意されているViewを活用して、モダンでMaterial Designらしいレイアウトを構築することができました。

この記事で着目した fitsSystemWindows の挙動についてはCollapsingToolbarLayoutを用いた実装では把握していなくても問題ありませんでしたが、自力でステータスバー部分を透過させたレイアウトを作るような際に大いに活用できる知識なのではないでしょうか。

参考サイト


Fablicでは、Material Designに準拠した使いやすいユーザーインターフェースの構築やAndroid Wearアプリの対応などプラットフォームへの最適化に力を入れてAndroidアプリの開発を行っています。このような環境で共にプロダクトを作り上げていきたいアプリエンジニアの方のご応募もお待ちしております!

*1:FrameLayoutなどの通常のViewGroupの fitsSystemWindows が true の場合、子のViewの onApplyWindowInsets にはシステムUI分のインセットを含まないWindowInsetsが渡されるため

*2:本当にステータスバーを非表示にしたい場合は SYSTEM_UI_FLAG_FULLSCREEN を指定する

*3:CoordinatorLayout.LayoutParamsに含まれるCoordinatorLayout内における自身の振る舞いを定義する専用のクラス