Android Design Support Libraryを使う

どうもこんにちは。Google I/O 2015 帰りの 英単語サプリ 担当 田澤です。

Material Designを実現するためのAndroid Design Support Libraryが発表されました。
これまではサードパーティーのライブラリを利用するか、独自実装してMaterial Design対応をする必要がありましたが、ついに公式でサポートされるようになりました。サポートOSバージョンはAndroid 2.1 以上となっています。

そこで、本記事ではAndroid Design Support Libraryで追加されたコンポーネントと使い方を紹介します。
また、各コンポーネントに関するDesign Guidelineのリンクも用意しているのでご参照ください。

なお、ここで紹介しているコードは Github - android-SampleDesignSupportLibrary で公開しています。

導入方法

build.gradleに下記を追加します。


使い方

以下に各コンポーネントについて紹介していきます。

Navigation View

( クリックして動画を再生 )

NavigationViewは、menu resourceからDrawerのNavigationItemのリストを作成することができます。
レイアウトはDrawerLayout内にContent Layoutと一緒にNavigationViewを配置することになります。

<android.support.v4.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- Your content layout -->
    <android.support.design.widget.NavigationView
        android:id="@+id/navigation_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="@color/brand_sub"
        app:headerLayout="@layout/drawer_header"
        app:itemIconTint="@drawable/drawer_text_selector"
        app:itemTextColor="@drawable/drawer_icon_selector"
        app:menu="@menu/drawer"/>
</android.support.v4.widget.DrawerLayout>

app:headerLayoutではヘッダーに表示したいレイアウトを指定します。
app:menu="@menu/drawer"には、DrawerのNavigationItemとして表示したいメニューを指定します。

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".NavigationViewActivity">
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/navigation_item_1"
            android:checked="true"
            android:icon="@drawable/ic_launcher"
            android:title="@string/navigation_item_1"/>
        <item
            android:id="@+id/navigation_item_2"
            android:icon="@drawable/ic_launcher"
            android:title="@string/navigation_item_2"/>
        <!-- Sub Headerを作成 -->
        <item
            android:id="@+id/navigation_sub_header"
            android:title="@string/navigation_sub_header">
            <menu>
                <item
                    android:id="@+id/navigation_sub_item_1"
                    android:icon="@drawable/ic_launcher"
                    android:title="@string/navigation_sub_item_1"/>
                <item
                    android:id="@+id/navigation_sub_item_2"
                    android:icon="@drawable/ic_launcher"
                    android:title="@string/navigation_sub_item_2"/>
            </menu>
        </item>
    </group>
</menu>

1つのitemごとにDrawerのNavigationItemとして作成されます。また、item内にmenuを入れ子にすることでSubHeaderを作成することも可能です。
なお、SubHeaderの中にさらにSubHeaderを作成しようとするとitem内のmenuは無視されNavigationItemは作成されません。

色のカスタマイズには以下の属性が用意されています。

内容
itemBackground NavigationItemのBackgroundColor
itemIconTint アイコンのtintColor
itemTextColor テキストのColor

NavigationItem選択時のハンドリングはNavigationView.OnNavigationItemSelectedListenerを介して行います。

mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(MenuItem menuItem) {
                int itemId = menuItem.getItemId();
                if (itemId == R.id.navigation_item_1 || itemId == R.id.navigation_item_2) {
                    // Navigation Itemを選択状態にしたい場合はsetCheckedをtrueにする
                    menuItem.setChecked(true);
                    mDrawerLayout.closeDrawers();
                    return true;
                }
            }
        });

NavigationViewの利点は、menu resourceを用意するだけでナビゲーション項目を追加することが可能なことです。
従来のNavigationDrawerの実装方法では自身でListViewを作成して処理を追加するなどの必要がありましたが、それらが不要となりました。
ただし、リスト形式ではなく複雑なレイアウトや少し凝ったことをする場合は、これまでのNavigationDrawerの実装方法のままの方が良いでしょう。

TextInputLayout

text_input_layout

Floating labelsを実現するためのコンポーネントです。
EditTextをTextInputLayoutの子Viewにするだけで使用できます。

<android.support.design.widget.TextInputLayout
        android:id="@+id/text_input_layout_1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="名前">
    </android.support.design.widget.TextInputLayout>

Floating labelにはEditTextで指定したhintが適用されます。
未入力やバリデーションエラーの場合のために、エラー文言を表示することもできます。

TextInputLayout textInputLayout = (TextInputLayout) findViewById(R.id.text_input_layout_2);
        textInputLayout.setErrorEnabled(true);
        textInputLayout.setError("エラー内容");

Floating Action Button

fab

お馴染みのFloating Action Button(FAB)も用意されています。
但し、ボタンとしての機能しか持ち合わせていません。
Speed dial(FABタップ時にFABからメニューを表示させたい場合)などを実現するには、独自に実装する必要があります。

<android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_marginBottom="16dp"
        android:layout_marginRight="16dp"
        android:src="@drawable/ic_launcher"
        app:fabSize="normal"/>

FABにはあらかじめ2つのサイズが提供されており、fabSizenormalminiを選択できます。
デフォルト値はnormalが選択されています。

背景色はデフォルトだと、colorAccentで指定されているものが適用されます。
カスタマイズは、FloatingActionButton#setBackgroundTintList(ColorStateList)で可能です。

注意事項

現時点で特定機種、OSバージョンでFABが丸くならない、影が付かないといったバグが報告されています。
これらのバグに対応するには以下の設定を追加する必要があります。


Snackbar


( クリックして動画を再生 )

ユーザーがフィードバック可能なToastのようなコンポーネントです。
画面下部に表示され、Actionを1つだけ追加することができます。
使用方法もほぼToastと同じです。showメソッドの呼び忘れに注意しましょう。

// No Action
    Snackbar.make(mParentLayout, "Test", Snackbar.LENGTH_SHORT).show();
// Add Action
    Snackbar.make(mParentLayout, "Action Test", Snackbar.LENGTH_LONG).
            setAction("Action", new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(getApplicationContext(), "Action", Toast.LENGTH_SHORT).show();
                }
            }).show();

Actionを追加するにはsetActionメソッドでView.OnClickListenerをセットします。

Snackbar snackbar = Snackbar.make(mParentLayout, "Test", Snackbar.LENGTH_SHORT).
        setActionTextColor(getResources().getColor(android.R.color.holo_red_dark));
    snackbar.getView().setBackgroundColor(getResources().getColor(R.color.bg_brand));
    snackbar.setAction("Action", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), "Action", Toast.LENGTH_SHORT).show();
            }
    }).show();

Snackbar#setActionTextColorでActionのテキストカラーを変更できます。
また、Snackbar#getViewから取得したViewにsetBackgroundColorをすることで、Snackbarの背景色を変更することができます。

FABを画面右下に配置している場合、Snackbarを表示するとFABの上に覆いかぶさることになってしまします。
これはデザインガイドラインとしてNGとなっています。正しい挙動としては、Snackbarが表示されるとFABがその分上に移動するものとなります。このような挙動はCoordinatorLayoutを使用することで対応できます。後述のCoordinatorLayoutをご参照ください。

TabLayout


( クリックして動画を再生 )

基本的な使用方法は以下のとおりとなります。

tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
    tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
    tabLayout.addTab(tabLayout.newTab().setText("Tab 3").setIcon(R.drawable.ic_add_white_24dp));

setTextでタイトル、setIconでアイコンをそれぞれ指定できます。

ViewPagerと連携させる場合は以下のとおりになります。

PagerAdapter pagerAdapter = new PagerAdapter(TabsViewPagerActivity.this, mViewPager);
        tabLayout.setTabsFromPagerAdapter(pagerAdapter);
        tabLayout.setupWithViewPager(mViewPager);
static class PagerAdapter extends FragmentPagerAdapter implements ViewPager.OnPageChangeListener {
        private ViewPager mViewPager;
        public PagerAdapter(AppCompatActivity activity, ViewPager viewPager) {
            super(activity.getSupportFragmentManager());
            mViewPager = viewPager;
            mViewPager.setAdapter(this);
            mViewPager.addOnPageChangeListener(this);
        }
        // --- 省略 ---
        @Override
        public CharSequence getPageTitle(int position) {
            return "Title " + position;
        }
    }

単にTabの選択状態とPagerの中身を連動させたい場合は、TabLayout#setupWithViewPager(ViewPager)だけで対応できます。
Tab変更タイミングを検知したい場合は、TabLayout#setOnTabSelectedListenerが用意されています。
TabのTitleはPagerAdapterのgetPageTitleの戻り値が使用されます。

GravityとMode

TabLayoutはGravityModeが指定できるようになっています。

tabLayout.setTabGravity(TabLayout.GRAVITY_CENTER);
        tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
Gravity
内容
GRAVITY_CENTER Tabを中心に置きます
GRAVITY_FILL Tabを画面いっぱい使用して表示します
GRAVITY_CENTER
GRAVITY_CENTER
GRAVITY_FILL
GRAVITY_FILL
Mode
内容
MODE_FIXED 全てのTabを画面内に表示させます。Tabの数が多いとTabが潰れて表示されます
MODE_SCROLLABLE Tabが横スクロール可能になります。Tabが長くなったり数が多い場合に有効です
MODE_FIXED
MODE_FIXED

CoordinatorLayout

CoordinatorLayoutは1つまたは複数のViewで相互関係を持たせることができるレイアウトです。

CoordinatorLayoutとその子Viewはanchorを持つことができます。
anchorとなったViewは、紐付いているViewが移動すると同様に移動するようになります。


( クリックして動画を再生 )

<android.support.design.widget.CoordinatorLayout
    android:id="@+id/coordinator_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
	<!-- 省略 -->
    <View
        android:id="@+id/square_view"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:layout_gravity="center"
        android:layout_marginTop="?attr/actionBarSize"
        android:background="#999999"/>
	<!-- anchorとなるView -->
    <View
        android:layout_width="16dp"
        android:layout_height="16dp"
        android:background="@color/bg_brand"
        app:layout_anchor="@id/square_view"/>
    <View
        android:layout_width="16dp"
        android:layout_height="16dp"
        android:background="@color/bg_brand"
        app:layout_anchor="@id/square_view"
        app:layout_anchorGravity="right"/>
    <View
        android:layout_width="16dp"
        android:layout_height="16dp"
        android:background="@color/bg_brand"
        app:layout_anchor="@id/square_view"
        app:layout_anchorGravity="bottom|right"/>
    <View
        android:layout_width="16dp"
        android:layout_height="16dp"
        android:background="@color/bg_brand"
        app:layout_anchor="@id/square_view"
        app:layout_anchorGravity="bottom|left"/>
</android.support.design.widget.CoordinatorLayout>

layout_anchorで紐づくViewのIDを指定します。指定するViewIDは、CoordinatorLayoutまたはその子Viewでなければなりません。
layout_anchorGravityでは、anchorとなったViewをどこに配置するかを指定します。

anchorとなったViewの動きはCoordinatorLayout.Behaviorを継承したクラス群が決めており、指定がない場合はDefaultのものが使用されます。

Defaultのanchorの動作について

layout_anchorGravity未指定の場合は、左上にanchorとなるViewが配置されます。
layout_anchorGravityでtop,bottom,left,rightを指定した場合、
anchorは基本的に紐付いたViewの端とanchorの中心部が重なるように配置されます。

anchorが画面からはみ出てしまう場合は自動的に画面内に収まるようにanchorの位置が調整されます。
紐付いたViewのサイズが画面いっぱい場合 且つ layout_anchorGravity="bottom|right"の場合はanchorの一部が画面右下にはみ出てしまうため、紐付いたViewの内側に配置されるよう調整されます。

以下は使用例の紹介となります。

例1. Floating action button scrolling

Snackbarを表示した時に、FABが上にスクロールするサンプルです。


( クリックして動画を再生 )

Snackbarの項目で紹介した通り、FABを右下に配置しているとSnackbarが上に覆いかぶさってしまいます。
この問題を回避するには以下のようにCoordinatorLayoutを使用します。CoordinatorLayoutによってSnackbarを表示すると自動的にFABが上方へ移動するようになります。
FABの動きは、CoordinatorLayout.Behaviorを継承したFloatingActionButton.Behaviorクラスが決めています。

<android.support.design.widget.CoordinatorLayout
    android:id="@+id/coordinator_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
   <!-- 省略 -->
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:layout_marginRight="16dp"
        android:src="@drawable/icon"
        app:fabSize="normal"
        app:layout_anchor="@id/coordinator_layout"
        app:layout_anchorGravity="bottom|right"/>
</android.support.design.widget.CoordinatorLayout>

CoordinatorLayoutとFABを紐付け、anchorとなるView(今回はFAB)はlayout_anchorGravityでbottom|rightを指定しているため右下に配置されます。

例2. ToolBar Scrolling(CoordinatorLayout + AppBarLayout)

ToolBarといったヘッダーコンポーネントとスクロールするViewを関連付け、スクロールにあわせてヘッダーコンポーネントの表示を切り替えます。

これはスクロールするとToolBarが上部に消えるサンプルです。
実現するにはCoordinatorLayoutの他にAppBarLayoutを使用します。


( クリックして動画を再生 )

AppBarLayout

中身は orientationをvertical指定したLinearLayoutで、CoordinatorLayoutの直下の子として使用されるのが想定されています。
Material DesignにおけるApp barScrolling Gesturesを実現するためのコンポーネントです。(ListのスクロールにあわせてToolBarが消えるといったinboxアプリやplay storeアプリに見られるような表現です)

レイアウトは以下のようなります。

<android.support.design.widget.CoordinatorLayout
    android:id="@+id/coordinator_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <! -- Your Scrollable View -->
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <android.support.v7.widget.Toolbar
            android:id="@+id/tool_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            android:minHeight="?attr/actionBarSize"
            app:layout_scrollFlags="scroll|enterAlways"/>
        <!-- 必要であればTabLayoutを追加 -->
        <!--<android.support.design.widget.TabLayout-->
        <!--android:layout_width="match_parent"-->
        <!--android:layout_height="wrap_content"/>-->
    </android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>``

AppBarLayoutの子View(Toolbar)にはスクロール時の動作を決めるlayout_scrollFlagsを設定しています。
設定できる値には以下のものがあります

内容
scroll Toobarを画面外に移動させたい場合につけます
enterAlways 下スクロールした時に即座にToolbarを表示します
enterAlwaysCollapsed 下スクロールしたときにリスト上部である場合、Toolbarを表示します。
exitUntilCollapsed ToolbarにminHeightが使用されている場合は、ToolbarはminHeightの分は表示状態で残ります。また、enterAlwaysCollapsedと同様にリスト上部に来た時点でToolbarを表示します。

AppBarLayoutはScrollable Viewと相互関係を持ち挙動を制御するために、Scrollable Viewに対しlayout_behaviorAppBarLayout.ScrollingViewBehaviorをバインドしています。
そうすることで、ユーザーがScrollable Viewをスクロールさせた時にAppBarLayoutは子Viewのlayout_scrollFlagsに応じたアニメーションを行うことができます。

Scrollable Viewについて

CoordinatorLayoutに配置するScrollable Viewには制限があります。
SupportLibrary v22.2RecyclerViewを使用するか、
Developers - AppBarLayout の通り、NestedScrollViewの子Viewとして配置するかになりそうです。

正しいScrollable Viewを配置しないとスクロールしてもToolbarは動いてくれません。

例3. Collapsing Toolbar(CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout)

CoordinatorLayout、AppBarLayoutに加えてCollapsingToolbarLayoutを使用します。

CollapsingToolbarLayout

CollapsingToolbarLayoutはAppBarを拡大縮小表示させるためのラッパーです。AppBarLayoutの子として使用するようになっています。


( クリックして動画を再生 )

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="200dp">
        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="#999999"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@drawable/pic"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax"/>
            <android.support.v7.widget.Toolbar
                android:id="@+id/tool_bar"
                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.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@drawable/ic_add_white_24dp"
        app:layout_anchor="@+id/app_bar"
        app:layout_anchorGravity="bottom|right|end"/>
</android.support.design.widget.CoordinatorLayout>

CollapsingToolbarLayoutにlayout_scrollFlags="scroll|exitUntilCollapsed"を指定することで、CollapsingToolbarLayout縮小時にToolbarの高さ分は表示された状態を保持します。また、同時に下スクロール時の画像表示タイミングを制御しています。(リスト上部が表示されるまで下スクロールするとCollapsingToolbarLayoutが展開されます)

Toobarにlayout_collapseMode="pin"を指定することで、CollapsingToolbarLayoutが縮小した時に画面上に残るようにしています。また、ImageViewにlayout_collapseMode="parallax"を指定することでCollapsingToolbarLayoutが縮小していくにつれて、徐々に透過され消えていくようになります。FABはAppBarLayoutのanchorとしているのでAppBarLayoutが非表示になると同時に消えます。

CollapsingToolbarLayoutのカスタマイズには以下の属性が用意されています。

内容
contentScrim CollapsingToolbarが縮小された時の背景色
expandedTitleMargin TitleのMargin
expandedTitleMarginBottom Title下のMargin
expandedTitleMarginEnd Title右のMargin
expandedTitleMarginStart Title左のMargin
CollapsingToolbarLayout collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
        collapsingToolbarLayout.setTitle("CollapsingToolbarLayout Title");

CollapsingToolbarLayout#setTitleでタイトルを指定するとCollapsingToolbarLayoutが縮小された時にToolbarにそのタイトルが表示されるようになります。


CoordinatorLayoutを使用する際には、基本的にJavaのコードを書く必要はありません。レイアウトのファイルは少々見辛くなりますが、楽に使用できる印象です。それだけでMaterial Designのアニメーションを実現できるので非常に便利なコンポーネントだと思います。また、独自でCoordinatorLayoutのBehaviorを作成もできるようなので拡張性もあります。是非導入したいコンポーネントです。

導入するにあたって

Design Support LibraryによってMaterial Designの導入のハードルが下がりました。
その一方で、正しくDesignに反映させなければ使いにくい見づらいアプリになってしまいかねません。

今一度Design Guidelineを読み直すことも必要かと思います。
また、GoogleではMaterial Design Awardsを開催し素晴らしいデサインのアプリを紹介しているので参考にしたいところです。

さいごに

ざっくりDesign Support Libraryの使い方を紹介しましたが、いかかでしたでしょうか?
開発効率アップとともに、よりよいデザインのアプリケーションが増えてユーザーの利便性が向上すればと思います。