Material Design な Navigation Drawer を目指す(1)


Google 謹製の Android アプリケーションを中心に、Material Design な Navigation Drawer を目にすることが多くなってきました。Material Design な Navigation Drawer については、Google 公式ガイドラインの Patterns > Navigation drawer に詳しく説明されていますが、従来の Drawer と比べて見た目で分かる主な違いとしては以下のような点があります。

  • ドロワーが開いたときにアクションバーの上に重なる
  • ステータスバーを透過させて、その下までドロワーを表示
  • 選択時のハイライト表示にアニメーション(RippleDrawable)

OLD_NEW

公式ガイドラインにあるような Material Design な Navigation Drawer を目指した結果、上記のような状態まで実装してみたので、その中身をご紹介していきます。Android5.0 Lollipop(SDK v21)を対象に作りましたので、新しい機能も一緒に利用しています。開発環境は Android Studio 1.1 Preview です。

  • ActionBar の変わりに、新しい ToolBar を使ってみる。
  • ListView の変わりに、新しい RecyclerView を使ってみる。

第1回目は ToolBar と RecyclerView を使って、単純なリストのみの Drawer までのご紹介で、第2回目は Header や Divider などの要素を含めた Drawer を作るところまでをご紹介する予定です。

Toolbar と RecyclerView を使って Drawer を作る

準備

android.support.v7.widget.Toolbar と android.support.v7.widget.RecyclerView は、従来からある ActionBar/ListView と比べてより柔軟にカスタマイズしやすくなったウィジェットです。ToolBar と RecyclerView をサポートしているライブラリが別途必要なので、build.gradle に下記のように追加します。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    ccompile 'com.android.support:recyclerview-v7:21.0.3'
}

また、適用するテーマを AppCompat.NoActionBar に変更しないと、すでに ActionBar があるよ!と怒られます。

values-v21/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    </style>
</resources>

activity_main.xml

従来の ActionBar はレイアウトに明示的に配置しなくとも表示されましたが、ToolBar は下記のように明示的に配置してあげる必要があります。

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- content -->
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

	<!-- Toolbar -->
        <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/colorAccent"
            android:elevation="4dp"/>

	<!-- Hello World! -->
        <TextView
            android:id="@+id/contentText"
            android:text="@string/hello_world"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true"
            android:textSize="56sp" />

    </RelativeLayout>

    <!-- nav drawer -->
    <android.support.v7.widget.RecyclerView
        android:id="@+id/drawer_view"
        android:scrollbars="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#fff"
        android:elevation="8dp"/>

</android.support.v4.widget.DrawerLayout>

配置した ToolBar の elevation プロパティを設定していますが、ここで指定した値によってドロップシャドウの効果が付きます。これは、Material Design の特徴の1つである、すべてのオブジェクトがXYZ軸を持っていて3D空間上に存在している事を表現する為の効果で、elevation でZ軸を表現しています。Toolbar の 4dp は、What is material? > Objects in 3D space の値を参考にしています。


MainActivity.java

Drawerに表示する内容をXMLで用意しておき、MainActivity で読込んで RecyclerView.Adapter に渡します。MainActivity は ActionBarActivity を拡張して作り、setSupportActionBar() メソッドで ToolBar を ActionBar として設定しています。

<string-array name="drawer_list_arr">
	<item>menu111</item>
	<item>menu222</item>
	<item>menu333</item>
</string-array>
public class MainActivity extends ActionBarActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // ツールバーをアクションバーとして使う
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        // UPナビゲーションを有効化する
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        // ドロワーの開け閉めをActionBarDrawerToggleで監視
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.string.app_name, R.string.app_name);
        mDrawerLayout.setDrawerListener(mDrawerToggle);

        // ドロワー開くボタン(三本線)を表示
        mDrawerToggle.setDrawerIndicatorEnabled(true);

        // RecyclerViewの設定
        setRecyclerView();
    }

    private void setRecyclerView(){
        mRecyclerView = (RecyclerView) findViewById(R.id.drawer_view);

        // RecyclerView内のItemサイズが固定の場合に設定すると、パフォーマンス最適化
        mRecyclerView.setHasFixedSize(true);

        // レイアウトの選択
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);

        // XMLを読込んで表示する
        String[] drawerMenuArr = getResources().getStringArray(R.array.drawer_list_arr);
        mAdapter = new SimpleDrawerAdapter(drawerMenuArr);

        mRecyclerView.setAdapter(mAdapter);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // アクションバー上のボタン選択時のハンドリング
        int id = item.getItemId();

        if (id == R.id.action_settings) {
            return true;
        }

        // ActionBarDrawerToggleにイベント渡す
        // 渡さないと、ドロワーボタンを押しても開かない
        if (mDrawerToggle.onOptionsItemSelected(item)) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);

        // ActionBarDrawerToggleとMainActivityの状態を同期する
        mDrawerToggle.syncState();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        // ActionBarDrawerToggleにイベント渡す
        mDrawerToggle.onConfigurationChanged(newConfig);
    }
}

SimpleDrawerAdapter.java

RecyclerView.Adapter では Drawer に表示する各アイテムのViewに対して、データをバインドしてます。RecyclerView は ListView と異なり、アイテムをタップ可能にしたり、その際にハイライト色をつけたりするには、明示的に書いておく必要があります。

public class SimpleDrawerAdapter extends RecyclerView.Adapter<SimpleDrawerAdapter.ViewHolder> {
    
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextView;
        public ViewHolder(View itemView, int viewType) {
            super(itemView);

            //各アイテムのViewを取得
            mTextView = (TextView) itemView.findViewById(android.R.id.text1);
        }
    }

    // MainActivityから渡されるデータ
    private String[] mDrawerMenuArr;

    public SimpleDrawerAdapter(String[] arrayList){
        mDrawerMenuArr = arrayList;
    }

    @Override
    public SimpleDrawerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        // レイアウトはsimple_list_item_1を利用
        View itemView = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);

        // アイテム選択可能にする
        itemView.setClickable(true);

        // アイテム選択時の Ripple Drawable を有効にする
        // Android 4 系端末で確認すると、Ripple効果は付かないが、選択色のみ適用される
        TypedValue outValue = new TypedValue();
        parent.getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
        itemView.setBackgroundResource(outValue.resourceId);

        // ViewHolderインスタンス生成
        ViewHolder vh = new ViewHolder(itemView, viewType);
        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {

        // 各アイテムのViewに、データをバインドする
        String menu = mDrawerMenuArr[position];
        holder.mTextView.setText(menu);
    }

    @Override
    public int getItemCount() {
        return mDrawerMenuArr.length;
    }
}

実行結果


次回の予定

今回は、ToolBar と RecyclerView を利用した Drawer で、シンプルなテキストのリストを表示するところまでご紹介しました。新しいウィジェットはカスタマイズが柔軟にはなったものの、特に RecyclerView は必要な機能は自分で用意する必要があったりするので、ListView で要件が満たせるのであれば、そちらを使った方が良いかもしれません。

次回の記事では、RecyclerView.Adapter で様々なタイプのアイテムを取り扱う場合(DividerやSubheaderやら)と、タップイベントを検知してメインコンテンツを切り替えるところまでをご紹介したいと思います。