Android

NavigationDrawerをカスタマイズする(e.g. フッタ−を付ける)方法

ListViewにはaddFooterViewというメソッドがありますが、NavigationViewにはそのようなメソッドはありません。
また、NavigationViewにExpandableListViewなどの他のViewGroupを用いたい、もしくは自由にカスタマイズしたい時があります。
今回はNavigationViewの下部に固定フッターを付けたいという状況下で話をします。

NavigationViewは入れ子にできる

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.NavigationView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/nav_view"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:fitsSystemWindows="true">

    <RelativeLayout
        android:id="@+id/nav_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!-- 後述 -->
    </RelativeLayout>
</android.support.design.widget.NavigationView>

通常、NavigationViewには子を持たせずapp:menu attributeを用いてmenuをinflateします。

app:menu attribute を用い、NavigationViewの子としてフッターのViewを持たせ、それに対してandroid:layout_gravity="bottom"を指定してあげれば、フッターが実現したように見えますが、フッターと重なったドロワーアイテムが見えなくなってしまいます。

今回はその解決法として独自ListView, Adapter等を作っていきます。
少し大変になってしまいますが、よりカスタマイズ性が高くなるでしょう。

NavigationViewの中身のレイアウトを作る

まず、NavigationViewの中身のレイアウトを作ります。
今回は、各MenuのアイテムはListViewを用いて表示し、フッターにはFrameLayoutを使います。
もちろんListViewや、FrameLayoutに関してはどんなViewGroupでもViewでも構いません。

なお、以下レイアウトに関しては公式ガイドラインに従います。

nav_view.xml
<RelativeLayout
android:id="@+id/nav_content"
android:layout_width="match_parent"
android:layout_height="match_parent">

<include
    android:id="@+id/nav_header"
    layout="@layout/nav_header" />

<ListView
    android:id="@+id/nav_menu_items"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_above="@+id/nav_footer"
    android:layout_below="@id/nav_header"
    android:layout_marginBottom="8dp"
    android:layout_marginTop="8dp" />

<FrameLayout
    android:id="@id/nav_footer"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:layout_alignParentBottom="true" />
</RelativeLayout>

Navigation Itemのレイアウトを作る

次に、NavigationItemのレイアウトを作ります。
今回は、一般的なDrawerのMenuの様に、アイコンとタイトルを持たせます。
ここももちろん自由にレイアウトを組むことができます。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/top_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="48dp">

    <ImageView
        android:id="@+id/icon"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_alignParentStart="true"
        android:layout_centerVertical="true"
        android:layout_marginStart="16dp"
        tools:src="@drawable/ic_menu_camera"
        android:contentDescription="@null" />

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginStart="32dp"
        android:layout_toEndOf="@id/icon"
        tools:text="Title"
        android:textColor="#000000"
        android:textSize="14sp" />
</RelativeLayout>

NavigationListAdapter 及び NavigationItemの実装

あらかたレイアウトを組むことができたので各クラスを作っていきます。

まずはMenuの各アイテムを表すNavigationItemクラス。

NavigationItem.java
public class NavigationItem {
    @IdRes
    private final int id;

    private final Drawable icon;
    private final String title;

    public NavigationItem(@IdRes int id, Drawable icon, String title) {
        this.id = id;
        this.icon = icon;
        this.title = title;
    }

    public int getId() {
        return id;
    }

    public Drawable getIcon() {
        return icon;
    }

    public String getTitle() {
        return title;
    }
}

特に言うことはありませんね。
id, icon, titleを持ちます。

次にカスタムAdapterを作ります。

NavigationListAdapter.java
public class NavigationListAdapter extends ArrayAdapter<NavigationItem> {
    private final Context context;
    private final int resource;
    private final List<NavigationItem> items;
    private final LayoutInflater inflater;

    private int selectedPosition = -1;

    public NavigationListAdapter(@NonNull Context context, @NonNull List<NavigationItem> items) {
        // 先程作成したItemのレイアウトを用いる
        super(context, R.layout.nav_item, items);

        this.context = context;
        this.items = items;
        this.resource = R.layout.nav_item;
        inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        View v;
        if (convertView != null) {
            v = convertView;
        } else {
            v = inflater.inflate(resource, null);
        }

        RelativeLayout topLayout = v.findViewById(R.id.topLayout);
        ImageView icon = v.findViewById(R.id.icon);
        TextView text = v.findViewById(R.id.title);

        NavigationItem item = items.get(position);
        icon.setImageDrawable(item.getIcon());
        text.setText(item.getTitle());

        // Itemが選択されているかどうかで条件分岐
        if (position == selectedPosition) {
            // 背景、アイコン、テキストのカラーを変更
            icon.setColorFilter(ContextCompat.getColor(context, R.color.colorPrimary), PorterDuff.Mode.SRC_IN);
            topLayout.setBackgroundResource(R.drawable.selected_nav_item);
            text.setTextColor(ContextCompat.getColor(context, R.color.colorPrimary));
        } else {
            icon.setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN);
            topLayout.setBackgroundColor(Color.TRANSPARENT);
            text.setTextColor(ContextCompat.getColor(context, android.R.color.primary_text_light));
        }

        return v;
    }

    // 選択しているアイテムをidで変更する
    public void changeSelectedItemById(int id) {
        for (int i = 0; i < items.size(); i++) {
            NavigationItem item = items.get(i);
            if (item.getId() == id) {
                this.selectedPosition = i;
            }
        }
        notifyDataSetInvalidated();
    }
}

詳細はコード内のコメントに書いています。

例によってNavigationのItemをクリックしたときは背景色や、アイコン、タイトルの色を変えないといけないため、その処理を書いています。

また、Itemがクリックされたときの背景のDrawableとしてselected_nav_item.xmlを定義します

v21/selected_nav_item.xml
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?android:colorControlHighlight">
    <item android:drawable="@color/selectedNavigationItemBackground"/>
</ripple>
xml/selected_nav_item.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/selectedNavigationItemBackground"/>
</selector>

V21以上に対して、Ripple Effectを追加するためにレイアウトを分けています。

また、colors.xmlにその色を追加しておきます。

<color name="selectedNavigationItemBackground">#E8E8E8</color>

Activityで使用する

最後にDrawerを用いるActivityでの処理を記述します。
今回の範囲に関係ない処理は省略しています。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.activity_main);
        final DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        ViewGroup navHeader = (LinearLayout) findViewById(R.id.nav_header);
        ListView navListView = (ListView) findViewById(R.id.nav_menu_items);
        View navFooter = findViewById(R.id.nav_footer);

        // NavigationItemのリストを取得
        final List<NavigationItem> navItems = ...

        final NavigationListAdapter adapter = new NavigationListAdapter(this, navItems);
        navListView.setAdapter(adapter);
        adapter.changeSelectedItemById(R.id.nav_main);

        // 区切り線を消す
        navListView.setDivider(null);

        navListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
                drawer.closeDrawer(GravityCompat.START);
                performNavigationItemSelectedAction(navItems.get(position).getId());
                adapter.changeSelectedItemById(position);
            }
        });
    }

    // navigationItemがクリックされた時のアクションを記述する
    private void performNavigationItemSelectedAction(@IdRes int id) {
        switch (id) {
            case R.id.nav_main:
                break;
        }
    }
}

こちらもあらからコメントに詳細を書いています。

何らかの方法を用いてnavItemsを取得してください。
例えばimenuのxmlをパースしてNavigationItemに変換するなどです。

他にも方法はあると思いますが、今回はこのようにしてNavigationDrawerの下部に固定フッターを追加しました。