前回の記事では、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();
注意しなければいけないのは、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();
CoordinatorLayout
このクラスはMaterialDesignならではだなぁという感じのViewGroupです。あるViewがスクロールした際に、連動して他のViewもスクロールするようなUIを作るために使用するクラスのようです。GoogleのInboxアプリでよく見ますね。
Support Libraryのリリースノートによると、Design Libraryのコンポーネントの多くは、このCoordinatorLayoutの子Viewになることを当てにするとあります。
Class Overviewでは、主なユースケースとして次の2点を挙げています。
- As a top-level application decor or chrome layout
- 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(); } });
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が重ならない動きになりました。
振る舞いの実装について
ところでこの場合、実際に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) { ... } } }
まとめ
- SnackbarはToastにアクションを追加したようなUIなので、うまく使えばアプリ内通知がスッキリしそうだと感じます
- CoordinatorLayoutは一見地味だけど、ユーザー操作に合わせてUIをアニメーションさせるのに強力な仕組みだと思います
- CoordinatorLayout.Behaviorは、あまり凝ったことしなければFABなどのデフォルトのBehaviorで十分だろうけど、カスタムも出来るので使い勝手が良さそう