Blog / 開発者ブログ

AppBarLayoutに影が付かない件

2015.6.5

Google I/O2015でAndroid Design Support Libraryなるものが発表されましたが、皆さん使ってみましたでしょうか?

 

“SupportDesignは単体のモジュールなのにTheme.AppCompatなしじゃまともに動かずAppCompatとがんじがらめ”

 

とか

 

“FloatingActionButtonがAPI15,16だと四角く表示される”

(app:borderWidth=”0dp”を追加したら直るよ!)

 

device-2015-06-05-180852 device-2015-06-05-183136

 

とか

 

“AppBarLayoutが完全に隠れた状態でActivityのライフサイクルが走ると復帰時に真っ白になる”

device-2015-06-05-184722 device-2015-06-05-184709

 

などなど、いきなり色々怪しいところがありますが、マテリアルデザインを実現するウィジェットが比較的楽に導入できるので僕は概ね気に入ってます。

特にAppBarLayoutは素晴らしく、このレイアウトを使うとスクロールと連動してToolbarの表示を動的に切り替えてくれてルック&フィールが素晴らしくなります(一応SupportDesignが出る前も存在したけど使い勝手がめっちゃ悪く実質オープンソース頼りでした)

ただそのAppBarLayoutでどうしても納得行かないことがあり…

 

Lollipop未満の端末でAppBarLayoutに影が付かない!

 

見た目重視のために使おうとする機能なのに、いざ実装してみたら見た目が残念とか洒落にならなくないでしょうか。Lollipop未満って世に出回ってるスマホの9割が該当するんですが!

症状としてはこんな感じです↓

名称未設定 1

と言っても影の表現自体、マテリアルデザインで導入された概念なんでLollipop未満の端末は知らんがなって言われたらそれまでなんですが…。

ただ同じく追加されたFloatingActionButtonなんかは一応、KitKatまでの端末でもちゃんと影を表示してくれてるんですね。(一応って書いたのは”app:borderWidth=0dp”にした時にしか影が出てこないという謎仕様のためなんですが、話がズレそうなのでここではスルーします。とにかく影は出るんです!)

名称未設定 2

しかもAppBarLayoutと言えば、実質ActionBarの役割を果たすレイアウトでもあるので影の表現はかなり重要です。なんてったって今までActionBarはデフォルトのスタイルが影有りでしたから。

名称未設定 3

 

ということで前置きが長くなったんですが、今回はAppBarLayoutに影を追加する方法を探ってみました。

 

原因を探る

まずAppBarLayoutのソースを見てみることにしました。

ViewCompat.setElevation(this, this.mTargetElevation);

Elevationをセットしてるだけでした。ElevationはLollipop以降でしか機能しないのでKitkatまでの端末で動かないのは当たり前です。これはAppBarLayoutが特殊というわけではなく、他のViewも全部そうです。

 

じゃあFloatingActionButtonはどうなの?と言うと、当然setElevationはしていて、

if(VERSION.SDK_INT >= 21) {
    this.mImpl = new FloatingActionButtonLollipop(this, delegate);
} else {
    this.mImpl = new FloatingActionButtonEclairMr1(this, delegate);
}

int maxContentSize = (int)this.getResources().getDimension(dimen.fab_content_size);
this.mContentPadding = (this.getSizeDimension() - maxContentSize) / 2;
this.mImpl.setBackgroundDrawable(background, this.mBackgroundTint, this.mBackgroundTintMode, this.mRippleColor, this.mBorderWidth);
this.mImpl.setElevation(elevation);

それとは別にKitkatまでの端末が対象のFloatingActionButtonEclairMr1の中で、

this.mShadowDrawable = new ShadowDrawableWrapper(this.mView.getResources(), new LayerDrawable(layers), this.mShadowViewDelegate.getRadius(), this.mElevation, this.mElevation + this.mPressedTranslationZ);
this.mShadowDrawable.setAddPaddingForCorners(false);
this.mShadowViewDelegate.setBackgroundDrawable(this.mShadowDrawable);

ちゃんとやってた!ShadowDrawableっての作って影としてセットしてくれてるんですね。ShadowDrawableWrapperの中見るとしっかりCanvasで描いてました。

つまりAppBarLayoutは他のViewと一緒で、ActionBar相当の機能だからといって特別何かやってくれてるわけではないんですね。むしろFloatingActionButtonがちゃっかりし過ぎてるってことなのかもしれません。

 

対策

まずおなじみのToolbarに影を付ける方法が使えるか試してみました。ToolbarはそれこそActionBarをフレームワークから切り離しViewとして扱えるようにしたものなんですが、こちらも影が付きません。当然影が付かないのを気にする開発者は多く、ググると下記のようにToolbarの下に影用のViewを置く回避方法がいっぱいヒットします。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />

    <View
        android:id="@+id/bottom_shadow"
        android:layout_width="match_parent"
        android:layout_height="3dp"
        android:layout_below="@+id/toolbar"
        android:background="@drawable/bottom_shadow" />

    <ListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/toolbar" />

</RelativeLayout>

名称未設定 2
ちなみにLollipop以降はElevationセットするだけでOKです。ただElevationの値はかなり細かく定められていて、Toolbarの場合4dpをセットします。

詳しくは → Elevation and shadows
で、この方法をAppBarLayoutにも適用してみようと、まずは単純にAppBarLayoutの中にToolbarと並ぶ感じで影用のViewを入れてみました。

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

        <View
            android:layout_width="match_parent"
            android:layout_height="3dp"
            android:background="@drawable/bottom_shadow" />
    </android.support.design.widget.AppBarLayout>

device-2015-06-05-195441のコピー
影の部分が透過しない!AppBarLayoutがLinearLayoutを継承してるから予想はしてたのでへこたれない。

 

次にAppBarLayout自体をViewGroupで囲んでみました。

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:layout_scrollFlags="scroll|enterAlways"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
        </android.support.design.widget.AppBarLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="3dp"
            android:background="@drawable/bottom_shadow" />
    </LinearLayout>

device-2015-06-05-195441のコピー
見た目はいけた!でもスクロールと連動してAppBarLayoutが隠れない!CordinatorLayoutの直下に置かないと動かないのかな?力尽きたのでソース調べる気になれず。

 

 

それならとコンテンツ側をViewGroupで囲って影を常にコンテンツに被せるようにしてみました。

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <View
            android:layout_width="match_parent"
            android:layout_height="3dp"
            android:background="@drawable/bottom_shadow" />
    </RelativeLayout>

名称未設定 2

上手く動いた!

ということでAppBarLayoutに影を付けたい場合は、コンテンツ側を囲むようにしましょうという話でした。