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

EDIT MODE

モバイルアプリ開発に関することを書いています

Design Support Library v22.2.0について Part 2

前回の記事では、TextInputLayoutとFloatingActionButtonについて書きました。 androhi.hatenablog.com

今回は、主にSnackbarとCoordinatorLayoutについて調べたことを、書いていきます。

SnackBar

Android 4.4まではアプリ内で簡易的な通知を行う際に、Toastを使った実装が多かったと思いますが、Android 5.0以降はSnackbarと呼ばれる実装を使うよう推奨されてます。

UIとしてはToastととてもよく似ていますが、Snackbarにはアクションを含めることが出来る点や、スワイプ操作で表示を消すことが出来る点などが異なっています。

詳細は、公式サイトのSnackbars&toastsが詳しいです。

Snackbarの表示

使い方はほぼToastと同じなので、以下のようなコードで表示させてみました。

final LinearLayout layout = (LinearLayout) findViewById(R.id.root_layout);
Snackbar.make(layout, "Snackbar test", Snackbar.LENGTH_LONG)
        .setAction("UNDO", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // something
            }
        })
        .show();

f:id:androhi:20150603021218p:plain

注意しなければいけないのは、make()メソッドの第一引数にSnackbarをホールドするための親となるViewを渡す必要があることです。 アクションを含めたいときは、setAction()メソッドにラベルとリスナーを渡すだけでした。

なお一つのSnackbarにアクションは一つしか含めることが出来ません。 メッセージは文字数が多い場合は、適時改行されます。 試しに以下のコードを実行すると、下図のようになりました。

final LinearLayout layout = (LinearLayout) findViewById(R.id.root_layout);
String message = "long test message. long test message. long test message. long test message.";
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
        .setAction("UNDO", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // something
            }
        })
        .setAction("REDO", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // something
            }
        })
        .show();

f:id:androhi:20150603021241p:plain

CoordinatorLayout

このクラスはMaterialDesignならではだなぁという感じのViewGroupです。あるViewがスクロールした際に、連動して他のViewもスクロールするようなUIを作るために使用するクラスのようです。GoogleのInboxアプリでよく見ますね。

Support Libraryのリリースノートによると、Design Libraryのコンポーネントの多くは、このCoordinatorLayoutの子Viewになることを当てにするとあります。

Class Overviewでは、主なユースケースとして次の2点を挙げています。

  1. As a top-level application decor or chrome layout
  2. As a container for a specific interaction with one or more child views

要約すると最初に書いたように、特別な依存関係を構築したいViewの親ViewGroupとして使えということのようです。

CoordinatorLayout + FloatingActionBar + Snackbar

分かり易い例として、Snackbarが表示されるとそれに合わせてFABもアニメーションする(Inboxと同じ動き)、というサンプルを動かそうと思います。

まずはCoordinatorLayoutクラスを、以下のように普通にViewGroupとして使ってみました。 実行してみると、SnackbarとFABが重なってしまいました。意図した動きになりません。

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:design="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/coordinator_layout"
    tools:context=".MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin">

        <Button
            android:id="@+id/show_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Show Snackbar"
            />

        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:src="@drawable/ic_add_white_24dp"
            design:fabSize="normal"
            />

    </RelativeLayout>
    
</android.support.design.widget.CoordinatorLayout>
findViewById(R.id.show_button).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        final CoordinatorLayout layout = (CoordinatorLayout) findViewById(R.id.coordinator_layout);
        String message = "Snackbar test.";
        Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
                .setAction("UNDO", new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // something
                    }
                })
                .show();
    }
});

f:id:androhi:20150603021308p:plain

FABにアンカーを設定する

片方のViewの動きに合わせて、もう片方のViewもアニメーションさせるには、anchorを設定しなければいけないとのことです。アンカーに指定出来るのは、CoordinatorLayoutの派生クラスになります。アンカーが設定されてるViewは指定出来ません。

さっきの失敗したxmlを、以下のように直してみました。

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:design="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/coordinator_layout"
    tools:context=".MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/show_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Show Snackbar"
            />

    </RelativeLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_add_white_24dp"
        design:fabSize="normal"
        design:layout_anchor="@id/coordinator_layout"
        design:layout_anchorGravity="bottom|right"
        />

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

やったことは、FABをCoordinatorLayout直下に移動して、anchorを設定しました。 layout_anchorに対象のViewIDを指定し、layout_anchorGravityで特定の位置に寄せています。

これを実行すると、きちんとSnackbarとFABが重ならない動きになりました。

f:id:androhi:20150603021328p:plain

振る舞いの実装について

ところでこの場合、実際にFABがアニメーションする仕組みを提供しているのは、FloatingActionButton.Behaviorクラスです。

これはCoordinatorLayout.Behaviorを拡張したクラスで、他にもAppBarLayout.Behavior/AppBarLayout.ScrollingViewBehavior/SwipeDismissBehaviorなどが用意されていて、それぞれどんなアニメーションをするかなどの振る舞いを実装しているようです。

CoordinatorLayoutの子Viewになるクラスには、DefaultBehaviorアノテーションを使うことで、カスタムしたBehaviorも設定可能なようです。試してませんが、以下のように使えば良さそうです。

@DefaultBehavior(CustomView.Behavior.class)
public class CustomView extends View {
    // something
    ...
    public static class Behavior extends android.support.design.widget.CoordinatorLayout.Behavior<CustomeView> {
        ...
    
        public boolean layoutDependsOn(CoordinatorLayout parent, CustomView child, View dependency) {
            ...
        }
    
        public boolean onDependentViewChanged(CoordinatorLayout parent, CustomeView child, View dependency) {
        ...
        }
    }
}

まとめ

  1. SnackbarはToastにアクションを追加したようなUIなので、うまく使えばアプリ内通知がスッキリしそうだと感じます
  2. CoordinatorLayoutは一見地味だけど、ユーザー操作に合わせてUIをアニメーションさせるのに強力な仕組みだと思います
  3. CoordinatorLayout.Behaviorは、あまり凝ったことしなければFABなどのデフォルトのBehaviorで十分だろうけど、カスタムも出来るので使い勝手が良さそう