LoginSignup
135
127

More than 5 years have passed since last update.

Material design / Shadow をより Shadow に

Last updated at Posted at 2014-12-08

こんにちは @sho5nn です。

この記事は Android Advent Calendar 2014 の9日目の記事です。ご覧いただきありがとうございます。

今日は以下の3つをお話したいと思います。

  • Material design / Shadow とは
  • Shadow をより Shadow に
  • Shadow をより Shadow にして得たノウハウなど

Material design / Shadow とは

Material design

Google I/O 2014 にて Google が提唱したデザインガイドラインです。プラットフォームやデバイスを限定することなく、原則に基づいて一貫性のある体験をユーザーに提供します。詳しくは以下のリンクや、その他の解説サイトをご覧ください。

Shadow

Material design では、UI のメタファーの1つに「紙」を取り入れており、それを表現する要素の1つに Shadow があります。

shadow.png

Shadow 描画時に参考になる値を記載した画像が、http://www.google.com/design/spec/material-design/introduction.html の中にあったのですが、何故か現在は見当たらないので、以下に載せておきます。(もしかしたら非推奨になったのかも…?)

shadow_2.png

上記の画像で説明すると、Shadow は TopShadowBottomShadow の 2枚のレイヤーに分かれて構成されています。z-depth = 5 を例にすると以下のようになります。

shadow_5.png

微妙に異なる値の Shadow が重なることで、他のマテリアルとの関係性を明確に示しながら空間を創り出す Shadow となります。

shadow_3.png

Shadow の描画方法

API level:21

API level:21 で追加された android.view.View#setElevation(float) を使えば簡単に実現可能です。面倒な 9-patch 画像の用意も、gradient を埋め込んだ shape を定義した hoge.xml を drawable フォルダにぶち込む必要もありません!やったね!

API level:20 以下

Api level:20 以下では、たとえ android.support.v4.view.ViewCompat#setElevation(View, float) を呼び出しても 何も起こりません。 ViewCompat クラスをご覧頂ければわかりますが、setElevation(View, float) が実装されていないからです…

http://www.reddit.com/r/androiddev/comments/2kd843/so_how_are_you_guys_doing_with_your_current/

9-patch 画像や、 hoge.xml を drawable フォルダにぶち込むなど、従来の方法であれば実現可能です。

Shadow をより Shadow に

Movement of material には、以下の文章が記述されています。

Material can move along any axis.

マテリアルは X,Y,Z 軸に沿って移動することが可能です。

shadow_7.gif

Z 軸に沿って移動すれば当然 Shadow も変化します。

shadow_6.gif

しかし、例えば単純に Shadow を z-depth=1 から 5 へ瞬間的に変化しただけでは、Material design の原則の1つである Motion provides meaning には程遠くなります。Shadow もアニメーションをしながら変化することで、より良い体験をユーザーに与えます。

例として、z-depth 1 から 5 へアニメーションをさせる場合、 TopShadowBottomShadow それぞれの変化する値は以下になります。

Top Shadow の変化

z-depth 1 z-depth 5
color 12% black 30% black
y - offset 1dp 19dp
blur 1.5dp 19dp

Bottom Shadow の変化

z-depth 1 z-depth 5
color 24% black 22% black
y - offset 1dp 15dp
blur 1dp 6dp

しかし、Shadow の描画方法によってはそれを表現することが難しく、私はいろいろと模索をしたのですが、とてつもなく頭が爆発しそうでした。

9-patch 画像で Shadow を描画してるなら…

9-patch 画像では、描画サイズに応じて伸縮はしますが、Blur (ぼかし) 効果や y軸のオフセットまでは変化させられません。color は、alpha を弄ればなんとかなりそうな気もしますが…頭が爆発します。

<shape> タグを用いた hoge.xml を読み込んで ShapeDrawable として描画しているなら…

<layer-list> タグで <shape> を何個も重ねたり <gradient> タグを駆使したりして、擬似的に Blur 効果を表現することは可能ですが、動的な変化をさせるとなると…頭が爆発します。


そこで私は、半ば強引な心意気で、Shadow の動的な変化を可能とする実装を試みました。

ZDepthShadowLayout

ソースコードはこちら

gif のため Blur 効果の見栄えが悪いですが実機では問題なく綺麗に Blur ってます

demo.gif


使い方はこんな感じです。

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

    <app.mosn.zdepthshadowlayout.ZDepthShadowLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        shadow:z_depth="z_depth1"
        shadow:z_depth_shape="rect"
        shadow:z_depth_padding="z_depth5">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="16dp"
            android:text="Change ZDepth"
            android:background="@android:color/white"/>

    </app.mosn.zdepthshadowlayout.ZDepthShadowLayout>

</RelativeLayout>

Shadow を付けたい View を、ZDepthShadowLayout に内包します。
実際に Shadow の描画を担っているのは ShadowView であり、ZDepthShadowLayout は自動的に ShadowView を index 0 へ addView します。

shadow_10.png

Attribute の指定は以下のものがあります。

<app.mosn.zdepthshadowlayout.ZDepthShadowLayout
    shadow:z_depth="z_depth1"
    shadow:z_depth_shape="rect"
    shadow:z_depth_padding="z_depth5"
    shadow:z_depth_animDuration="150"
    shadow:z_depth_doAnim="true" />
Attribute 説明
z-depth Shadow の深さ
z_depth_shape Shadow の形
z-depth_animDuration z-depth 変更時のアニメーション時間
z-depth_doAnim z-depth 変更時のアニメーション可否
z-depth_padding Blur を描画するのに必要な余白
z-depth_paddingLeft Blur を描画するのに必要な左側の余白
z-depth_paddingTop Blur を描画するのに必要な上側の余白
z-depth_paddingRight Blur を描画するのに必要な右側の余白
z-depth_paddingBottom Blur を描画するのに必要な下側の余白
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ZDepthShadowLayout">
        <attr name="z_depth" format="enum">
            <enum name="z_depth0" value="0"/>
            <enum name="z_depth1" value="1"/>
            <enum name="z_depth2" value="2"/>
            <enum name="z_depth3" value="3"/>
            <enum name="z_depth4" value="4"/>
            <enum name="z_depth5" value="5"/>
        </attr>
        <attr name="z_depth_shape" format="enum">
            <enum name="rect" value="0"/>
            <enum name="oval" value="1"/>
        </attr>
        <attr name="z_depth_padding" format="enum">
            <enum name="z_depth0" value="0"/>
            <enum name="z_depth1" value="1"/>
            <enum name="z_depth2" value="2"/>
            <enum name="z_depth3" value="3"/>
            <enum name="z_depth4" value="4"/>
            <enum name="z_depth5" value="5"/>
        </attr>
        <attr name="z_depth_animDuration" format="integer"/>
        <attr name="z_depth_doAnim" format="boolean"/>
    </declare-styleable>
</resources>

android.support.v7.widget.Toolbar にも対応

多少やっつけ感はありますが、左右と上部の padding を無くすことで android.support.v7.widget.Toolbar にも対応できます。

<app.mosn.zdepthshadowlayout.ZDepthShadowLayout
    android:id="@+id/zDepthShadowLayout_toolBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    shadow:z_depth="z_depth1"
    shadow:z_depth_shape="rect"
    shadow:z_depth_paddingLeft="z_depth0"
    shadow:z_depth_paddingTop="z_depth0"
    shadow:z_depth_paddingRight="z_depth0"
    shadow:z_depth_paddingBottom="z_depth5">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="?attr/actionBarSize"
        android:background="?attr/colorPrimary"/>

</app.mosn.zdepthshadowlayout.ZDepthShadowLayout>

Animation

TopShadowBottomShadow の color, y-offset, blur の各値を ValueAnimator を用いて変化させ、onAnimationUpdate が呼ばれるたびに invalidate() を呼び出して再描画しています。

protected void changeZDepth(ZDepth zDepth) {

    int   newAlphaTopShadow      = zDepth.getAlphaTopShadow();
    int   newAlphaBottomShadow   = zDepth.getAlphaBottomShadow();
    float newOffsetYTopShadow    = zDepth.getOffsetYTopShadowPx(getContext());
    float newOffsetYBottomShadow = zDepth.getOffsetYBottomShadowPx(getContext());
    float newBlurTopShadow       = zDepth.getBlurTopShadowPx(getContext());
    float newBlurBottomShadow    = zDepth.getBlurBottomShadowPx(getContext());

    if (!mZDepthDoAnimation) {
        mZDepthParam.mAlphaTopShadow        = newAlphaTopShadow;
        mZDepthParam.mAlphaBottomShadow     = newAlphaBottomShadow;
        mZDepthParam.mOffsetYTopShadowPx    = newOffsetYTopShadow;
        mZDepthParam.mOffsetYBottomShadowPx = newOffsetYBottomShadow;
        mZDepthParam.mBlurTopShadowPx       = newBlurTopShadow;
        mZDepthParam.mBlurBottomShadowPx    = newBlurBottomShadow;

        mShadow.setParameter(mZDepthParam, mZDepthPadding, mZDepthPadding, getWidth() - mZDepthPadding, getHeight() - mZDepthPadding);
        invalidate();
        return;
    }

    int   nowAlphaTopShadow      = mZDepthParam.mAlphaTopShadow;
    int   nowAlphaBottomShadow   = mZDepthParam.mAlphaBottomShadow;
    float nowOffsetYTopShadow    = mZDepthParam.mOffsetYTopShadowPx;
    float nowOffsetYBottomShadow = mZDepthParam.mOffsetYBottomShadowPx;
    float nowBlurTopShadow       = mZDepthParam.mBlurTopShadowPx;
    float nowBlurBottomShadow    = mZDepthParam.mBlurBottomShadowPx;

    PropertyValuesHolder alphaTopShadowHolder     = PropertyValuesHolder.ofInt("alphaTopShadow",
            nowAlphaTopShadow,
            newAlphaTopShadow);
    PropertyValuesHolder alphaBottomShadowHolder  = PropertyValuesHolder.ofInt("alphaBottomShadow",
            nowAlphaBottomShadow,
            newAlphaBottomShadow);
    PropertyValuesHolder offsetTopShadowHolder    = PropertyValuesHolder.ofFloat("offsetTopShadow",
            nowOffsetYTopShadow,
            newOffsetYTopShadow);
    PropertyValuesHolder offsetBottomShadowHolder = PropertyValuesHolder.ofFloat("offsetBottomShadow",
            nowOffsetYBottomShadow,
            newOffsetYBottomShadow);
    PropertyValuesHolder blurTopShadowHolder      = PropertyValuesHolder.ofFloat("blurTopShadow",
            nowBlurTopShadow,
            newBlurTopShadow);
    PropertyValuesHolder blurBottomShadowHolder   = PropertyValuesHolder.ofFloat("blurBottomShadow",
            nowBlurBottomShadow,
            newBlurBottomShadow);

    ValueAnimator anim = ValueAnimator
            .ofPropertyValuesHolder(
                    alphaTopShadowHolder,
                    alphaBottomShadowHolder,
                    offsetTopShadowHolder,
                    offsetBottomShadowHolder,
                    blurTopShadowHolder,
                    blurBottomShadowHolder);
    anim.setDuration(mZDepthAnimDuration);
    anim.setInterpolator(new LinearInterpolator());
    anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int   alphaTopShadow     = (Integer) animation.getAnimatedValue("alphaTopShadow");
            int   alphaBottomShadow  = (Integer) animation.getAnimatedValue("alphaBottomShadow");
            float offsetTopShadow    = (Float) animation.getAnimatedValue("offsetTopShadow");
            float offsetBottomShadow = (Float) animation.getAnimatedValue("offsetBottomShadow");
            float blurTopShadow      = (Float) animation.getAnimatedValue("blurTopShadow");
            float blurBottomShadow   = (Float) animation.getAnimatedValue("blurBottomShadow");

            mZDepthParam.mAlphaTopShadow = alphaTopShadow;
            mZDepthParam.mAlphaBottomShadow = alphaBottomShadow;
            mZDepthParam.mOffsetYTopShadowPx = offsetTopShadow;
            mZDepthParam.mOffsetYBottomShadowPx = offsetBottomShadow;
            mZDepthParam.mBlurTopShadowPx = blurTopShadow;
            mZDepthParam.mBlurBottomShadowPx = blurBottomShadow;

            mShadow.setParameter(mZDepthParam, mZDepthPadding, mZDepthPadding, getWidth() - mZDepthPadding, getHeight() - mZDepthPadding); 

            invalidate();
         }
     });
    anim.start();
}

Shadow をより Shadow にして得たノウハウなど

View のサイズからはみ出た blur は切り取られる

仮に、以下のように 100x100 の ShadowView の中で ShapeDrawable を描画したとします。

Shadow
// ShadowView のサイズは 100 x 100
int viewWidth = 100;
int viewHeight = 100;

// ShapeDrawable は ShadowView のサイズにちょうど収まるようにする
mRectF = new RectF();
mRectF.left   = 0;
mRectF.top    = 0;
mRectF.right  = viewWidth;
mRectF.bottom = viewHeight;

ShapeDrawable shape = new ShapeDrawable(new RectShape());
Paint paint = shape.getPaint();
paint.setColor(Color.argb(56, 0, 0, 0));
paint.setMaskFilter(new BlurMaskFilter(blurPx, BlurMaskFilter.Blur.NORMAL));

canvas.drawRect(rect, paint);

期待した表示は、ShadowView のまわりに blur が描画されたものでしたが、

shadow_8.png

実際の表示は、ShadowView からはみ出た部分は切り取られてしまいました。

shadow_9.png

なので、blur をかける部分の大きさも考慮しつつ ShapeDrawableShadowView のサイズを決める必要がありました。

各 View のサイズの決め方

blur の大きさを考慮しつつ ShadowView のサイズを決定する必要がありますが、ZDepthShadowLayout に add される 他の View のうち最も大きい View に合わせて ShadowView のサイズを決定する必要もあります。

そこで以下の画像のようなレイアウト構成で組みました。
例として、add された 他の View が 200x200 、blur の大きさが 50 の場合です。

shadow_11.png

ZDepthShadowLayout には、blur の大きさ分の padding を指定します(実際はY軸の offset 分も足してます)。ShadowView のサイズは、ZDepthShadowLayout のサイズと同じにします(padding を無視)。

そして、ZDepthShadowLayout に対して ViewGroup#setClipToPadding(false) を呼び出すことで、ZDepthShadowLayoutpadding 領域に描画されている ShadowView もしっかり描画されます。

雑感

Material design に対応するのはかなり難しいと思います。まず、

  • Material design が提唱された理由
  • Material design が目指すところ
  • Material design の原理・原則

これらを踏まえた上で、 Material design に沿う理由、沿うことでアプリケーションはどうなるか、沿うことでユーザーに何を与えるのか、などなどデザイナーもエンジニアも理解していきたいところですね。

135
127
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
135
127