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


前回のMaterial Design な Navigation Drawer を目指す(1)では、新しいウィジェット ToolBar と RecyclerView を利用して、シンプルなテキストのリストを表示する Drawer をご紹介しました。今回の記事では、Drawer 内に Header や Divider などの複数要素を含めた Drawer までをご紹介します。

OLD_NEW

各アイテムのレイアウトを設定する

今回作成した Drawer では、Header / Menu / SubHeader / Divider の4つのアイテムを表示します。アイテム毎にレイアウト.xmlを作成し、この際のマージンやテキストサイズなどは、Patterns > Navigation drawer で記載されているマージンを参考に設定しますが、特段明記されていない箇所は適当な値になっています。下記のXMLは menu 部分のレイアウトの例です。

Drawer_part

res/layout/drawer_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:layout_gravity="start"
    android:background="?android:attr/selectableItemBackground">

    <ImageView
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:id="@+id/menu_icon"
        android:contentDescription="@string/desc_icon"
        android:layout_marginStart="16dp"
        android:layout_centerVertical="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:id="@+id/menu_text"
        android:layout_marginStart="72dp"
        android:layout_marginEnd="16dp"
        android:textSize="14sp"
        android:textColor="#D9000000"
        android:gravity="center_vertical"
        android:layout_centerVertical="true" />

</RelativeLayout>

アイテムの表示設定XMLを作成する

今回の複数アイテムを含む Drawer を作るに当たって、どのアイテムをどの順番でどの内容で表示するかを下記のようにXMLで設定してます。下記の例のような構成で menu アイテムの表示設定と、Drawer 全体の構成を設定するようにしました。

res/values/drawer_data.xml

    <!-- アイテム種別 type -->
    <array name="type_menu">
        <item name="key">type</item>
        <item name="value">menu</item>
    </array>
    ~~~
    <!-- 表示内容・設定 content -->
    <array name="menu_icon">
        <item name="key">icon</item>
        <item name="value">@drawable/test_image_w100x100</item>
    </array>
    <array name="menu_text_1">
        <item name="key">text</item>
        <item name="value">menu111</item>
    </array>
    ~~~
    <!-- アイテム内の構造 item -->
    <array name="menu1">
        <item>@array/type_menu</item>
        <item>@array/menu_icon</item>
        <item>@array/menu_text_1</item>
    </array>
    ~~~
    <!-- ドロワーの全体構造 menu -->
    <array name="drawer_menu_list">
        <item>@array/header</item>
        <item>@array/menu1</item>
        <item>@array/menu2</item>
        <item>@array/menu3</item>
        <item>@array/divider1</item>
        <item>@array/sub_header1</item>
        <item>@array/menu4</item>
        <item>@array/menu5</item>
    </array>

表示設定XMLを変換して RecyclerView.Adapter に渡す

今回作成したXMLではアイテム内の構造情報を key-value 形式で保持するようにしているので、HashMap のリストに変換して RecyclerView.Adapter に渡しています。

// XML
TypedArray drawerMenuList = getResources().obtainTypedArray(R.array.drawer_menu_list);
int menuLength = drawerMenuList.length();

// RecyclerView.Adapter に渡すデータ
ArrayList<HashMap<String, Object>> drawerMenuArr = new ArrayList<HashMap<String, Object>>();

for (int i = 0; i < menuLength ; i++) {
    TypedArray itemArr = getResources().obtainTypedArray(drawerMenuList.getResourceId(i, 0));
    int itemLength = itemArr.length();
    HashMap<String,Object> content = new HashMap<String, Object>();
    drawerMenuArr.add(content);
    for (int j = 0; j < itemLength ; j++) {
        TypedArray contentArr = getResources().obtainTypedArray(itemArr.getResourceId(j, 0));

        // key-value
        if(contentArr.getString(0).contains("icon")) {
            content.put(contentArr.getString(0), contentArr.getDrawable(1));
        }else{
            content.put(contentArr.getString(0), contentArr.getString(1));
        }
        contentArr.recycle();
    }
    itemArr.recycle();
}

複数アイテムを制御する RecyclerView.Adapter を用意

前回の SimpleDrawerAdapter ではテキスト1種を1箇所に表示するだけでしたが、今回の利用する Adapter ではテキストや画像リソースなど複数種類の設定を、それぞれレイアウト設定に合わせて表示・反映しています。getItemViewType()でアイテムの種類(header/menuなど)を判別できるので、あとは各メソッド内でViewTypeに合わせて処理を行います。

onCreateViewHolder:各アイテムのタイプに合わせて、ViewHolderを作成する


@Override
public DrawerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView;
    switch (viewType){
        case R.array.type_menu:
             // menu 用のレイアウトを利用する
             itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.drawer_menu, parent, false);
             itemView.setClickable(true);
             break;
        ~~~
     }
     ViewHolder vh = new ViewHolder(itemView, viewType);
     return vh;
}

RecyclerView.ViewHolder:各アイテムのitemViewから各Viewを取得する


public static class ViewHolder extends RecyclerView.ViewHolder {
   public ViewHolder(View itemView, int viewType) {
       super(itemView);
       switch (viewType){
           case R.array.type_menu:
                // menu 用レイアウトに含まれる、各Viewを取得
                mTextView = (TextView) itemView.findViewById(R.id.menu_text);
                mIconImageView = (ImageView) itemView.findViewById(R.id.menu_icon);
                break;
           ~~~
       }
   }
}

onBindViewHolder:各アイテムのViewに、作成したデータをバインドする


public void onBindViewHolder(ViewHolder holder, int position) {
    HashMap<String, Object> menu = mDrawerMenuArr.get(position);
    switch (holder.getItemViewType()){
        case R.array.type_menu:
           // ViewHolder で取得したViewに表示するデータをバインド
           holder.mTextView.setText(menu.get("text").toString());
           holder.mIconImageView.setImageDrawable((Drawable) menu.get("icon"));
           break;
        ~~~
    }
}

getItemViewType:各アイテムの種類(タイプ)を取得する


public int getItemViewType(int position) {
    HashMap<String,  Object> menu = mDrawerMenuArr.get(position);
    switch (menu.get("type").toString()){
        case "menu":
            return R.array.type_menu;
        ~~~
    }
}

Menu アイテムのタッチイベントを受け取り、コンテンツを切り替える

RecyclerView では OnItemTouchListener でタッチイベントを検知することができますが、 ListView#OnClickListener のように直接的には position の値が取得できません。
下記の例では、OnItemTouchListener で取得できるタッチ座標から、タッチしている View を取得し、その View から position の値を取得して、タッチした Menu アイテムに応じたテキストを表示しています。

// GestureDetectorを使って、onSingleTapUpを検知
mGestureDetector = new GestureDetector(getApplicationContext(),
        new GestureDetector.SimpleOnGestureListener() {
            @Override public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }
        });

mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
    @Override
    public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
        if (mGestureDetector.onTouchEvent(e)){

            // onSingleTapUpの時に、タッチしているViewを取得
            View childView = view.findChildViewUnder(e.getX(), e.getY());
            int potision = mRecyclerView.getChildPosition(childView);

            // タッチしているViewのデータを取得
            HashMap<String, Object> data = drawerMenuArr.get(potision);

            // Menu アイテムのみ
            if (data.get("type").toString().equals("menu")) {
                // 選択されたアイテムに応じてメインコンテンツを切替
                TextView contentView = (TextView) findViewById(R.id.contentText);
                contentView.setText((String) data.get("text"));

                // ドロワー閉じる
                mDrawerLayout.closeDrawers();
                return true;
            }
        }
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    }
});

ステータスバー透過させて、その下までドロワーを表示

ここまでで概ね Drawer としての体裁が整いましたが、最後にステータスバーの透過の設定をしてPatterns > Navigation drawerの Drawer により近づけたいと思います。
res/values-v21/styles.xml

<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowTranslucentStatus">true</item>
    </style>
</resources>

透過自体は、Style に windowTranslucentStatus=”true” を記述するだけですが、この設定だけではToolBar自体もステータスバーの下に潜り込んでしまいます。
これを回避する為、ToolBar の fitsSystemWindows=”true” を追記します。 

<android.support.v7.widget.Toolbar
~~~
android:fitsSystemWindows="true"/>

fitsSystemWindows

実行結果