【Android/Kotlin】いまさら聞けない?NavigationDrawerのメニューをカスタマイズする方法

【Android/Kotlin】いまさら聞けない?NavigationDrawerのメニューをカスタマイズする方法

今回は、ハンバーガーメニューを左上に表示、タップすることでメニューが表示されるアプリを作っていきます。

Material Designとして定義されて4、5年立つので、ほとんどのアプリで採用されているものですね〜

例えば、メルカリ

Android端末を利用している人で、使ったことがない人はいないでしょう。

使うのはNavigationDrawerになります。

AndroidStudioで、アクティビティ作成時のGalleryの1つにあるもあり、作ること自体は難しいものではありません。

ただ、Galleryで作ったものですとカスタマイズがイマイチなので、メニュー自体を自作する方法を紹介していきます。

AndroidStudioのGalleryでベースのアプリを作る

まずは、ベースのアプリを作ります。

そちらをカスタマイズしていく流れで説明をしていきます。

GalleryからのActivity作成

AndroidStudioにてappを選択後、New → Activity → Galleryを選択してください。

こちらのダイアログが表示されますので、Navigation Drawer Activityを選択してください。

動くところまで実装されていますので、そのままアプリを実行してみてください。

メニューの表示と動作が見れると思います。

ここまで作ってくれるなんて…、簡単に作れるようになったものですw

メニュー表示のレイアウト

メニューは NavigationView を利用して表示されています。

この記述だけで、左側のメニューが表示されることになります。

<android.support.design.widget.NavigationView
   android:id="@+id/nav_view"
   android:layout_width="wrap_content"
   android:layout_height="match_parent"
   android:layout_gravity="start"
   android:fitsSystemWindows="true"
   app:headerLayout="@layout/nav_header_main"
    app:menu="@menu/activity_main_drawer" />

app:headerLayoutという指定あがあると思いますが、こちらがメニュー上部のヘッダのレイアウトになります。

先程のスクショでいいうと、ドロイド君の顔と名称、アドレスが表示されている部分です。

nav_header_main.xmlというレイアウトファイルが作成されていますので、詳細はそちらで確認してみてください。

また、一番下に app:menu があります。

この設定にてメニュー用の定義ファイルを読み込み、メニューを表示しています。

そのメニューファイルはこちら

<?xml version="1.0"encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:showIn="navigation_view">

    <group android:checkableBehavior="single">
        <item
           android:id="@+id/nav_camera"
           android:icon="@drawable/ic_menu_camera"
            android:title="Import" />
        <item
           android:id="@+id/nav_gallery"
           android:icon="@drawable/ic_menu_gallery"
            android:title="Gallery" />
        <item
           android:id="@+id/nav_slideshow"
           android:icon="@drawable/ic_menu_slideshow"
            android:title="Slideshow" />
        <item
           android:id="@+id/nav_manage"
           android:icon="@drawable/ic_menu_manage"
            android:title="Tools" />
    </group>

    <item android:title="Communicate">
        <menu>
            <item
               android:id="@+id/nav_share"
               android:icon="@drawable/ic_menu_share"
                android:title="Share" />
            <item
               android:id="@+id/nav_send"
               android:icon="@drawable/ic_menu_send"
                android:title="Send" />
        </menu>
    </item>
</menu>

何となく分かると思いますが、group、item、menuがあり、こちらを設定することで表示内容がきまります。

<group>はメニューのグループ化をするものですね。囲むと最後のメニューの下に下線が表示されます。

<item> <menu> <item>の入れ子にすると、タイトルを付けることができます。

こちらがデフォルトで作成されているものの説明になります。

App:menuでの対応も簡単に、かつきれいに出力されるのでOKとも言えますが、カスタマイズ性に欠けるので、こちらをベースにカスタマイズしていきます。

Advertisement

自作レイアウトでメニュー表示

今回、実装していくのは、こちらの通り。

  • メニューリストを表示
  • メニューの下に固定画像を表示
  • メニューがタップされた場合、画面のメッセージを変更

難しいことをするのではなく、レイアウトを自分で作って対応していくイメージです。

自作したレイアウトファイルでメニューを表示する

ベースアプリでは、menuファイルの読み込みをしていたところを、自作したLayoutにて対応するように変更していきます。

メニューリストの表示をRecycleyViewを使って実装していきます。

RecyclerViewとはなんぞや?という方は、こちらを参照ください。

http://marketable-skill.biz/?p=805

レイアウトの修正

まずは、RecyclerViewを追加したレイアウトです。メニュー下部に画像(ImageView)の追加もしています。

<?xml version="1.0"encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/drawer_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <include
       layout="@layout/app_bar_main"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

    <android.support.design.widget.NavigationView
       android:id="@+id/nav_view"
       android:layout_width="wrap_content"
       android:layout_height="match_parent"
       android:layout_gravity="start"
       android:fitsSystemWindows="true" >

       <RelativeLayout
           android:layout_width="match_parent"
           android:layout_height="match_parent">

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

           <viewclass="android.support.v7.widget.RecyclerView"
               xmlns:tools="http://schemas.android.com/tools"
               android:id="@+id/nav_recycler_view"
               android:layout_above="@+id/nav_footer"
               android:layout_below="@+id/nav_header"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:layout_marginTop="4dp"
               android:layout_marginBottom="4dp"
               tools:listitem="@layout/nav_items" />

            <FrameLayout
               android:id="@+id/nav_footer"
               android:layout_width="280dp"
               android:layout_height="80dp"
                android:layout_alignParentBottom="true">
                <ImageView
                   android:id="@+id/footer_img"
                   android:src="@drawable/img_280x80"
                   android:scaleType="fitCenter"
                   android:layout_width="match_parent"
                   android:layout_height="wrap_content"
                   android:layout_marginLeft="5dp"
                   android:layout_marginRight="5dp"
                   android:layout_marginBottom="2dp"/>
            </FrameLayout>
        </RelativeLayout>
    </android.support.design.widget.NavigationView>
</android.support.v4.widget.DrawerLayout>

このようなイメージになります。

Cardview(リスト1行分の表示)の設定です。

メニューのところなので、アイコンとタイトルを追加しています。

<?xml version="1.0"encoding="utf-8"?>

<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:minHeight="48dp"
   android:id="@+id/nav_cardview"
   card_view:cardElevation="2dp"
    android:foreground="?android:attr/selectableItemBackground" >

    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:minHeight="48dp"
        android:id="@+id/nav_relative">

        <ImageView
           android:id="@+id/item_icon"
           android:layout_width="24dp"
           android:layout_height="24dp"
           android:layout_marginLeft="10dp"
           android:layout_alignParentStart="true"
           android:layout_centerVertical="true"
            />

        <TextView
           android:id="@+id/item_title"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="Menu Title"
           android:textColor="#000"
           android:textSize="15sp"
           android:layout_toEndOf="@+id/item_icon"
           android:layout_marginStart="32dp"
           android:layout_centerVertical="true"
            />
    </RelativeLayout>
</android.support.v7.widget.CardView>

こんなイメージですね。

リスト表示(Adapter/ViewHolder/Activity)

RecyclerViewを使う場合は、adapterとViewHolderを作り、それらを使ってレイアウトを表示する部分をActivityに追加していきます。

まずは1行分のデータ(メニューのアイコンとタイトル)を保持クラスを作成します。

なんで?って思うかもしれませんが、データを受け渡しなどで便利になるためですね。

import android.graphics.drawable.Drawable

class MenuItemInfo ( var id : Int,
                     var icon : Drawable,
                     var title : String)

次にAdapterです。

import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

open class MenuItemAdapter(private val mParentActivity: MainActivity,
                       private val mValue:ArrayList<MenuItemInfo>) : RecyclerView.Adapter<MenuViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MenuViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.nav_items, parent,false)
        val holder = MenuViewHolder(view)

        holder.itemView.setOnClickListener{
           val position = holder.adapterPosition
           onListClicked(view, position)
        }
        return holder
    }

    open fun onListClicked(view: View?, position: Int) {

    }

    override fun getItemCount(): Int {
        returnmValue.size
    }

    override fun onBindViewHolder(holder: MenuViewHolder, position: Int) {

        val item = mValue[position]
        holder.mIcon.setImageDrawable(item.icon)
        holder.mTitle.text = item.title
    }
}

1点補足ですが、RecyclerViewのタップを検知するためには、ViewHolderにリスナ登録が必要です。

RecyclerViewにはListViewにあるsetOnClickListenerが用意されておらず、ViewHolderクラスにて実装する必要があるためです。

Advertisement

ViewHolderにリスナを登録しているところと、

 holder.itemView.setOnClickListener{
     val position = holder.adapterPosition
     onListClicked(view, position)
 }

リスナ内で呼び出しているメソット定義しています。

open fun onListClicked(view: View?, position: Int) {

}

続いて、ViewHolderになります。

import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import kotlinx.android.synthetic.main.nav_items.view.*

class MenuViewHolder(mView : View) : RecyclerView.ViewHolder(mView) {
    val mIcon : ImageView = mView.item_icon
    val mTitle : TextView = mView.item_title
}

最後にActivityの実装です。

class MainActivity : AppCompatActivity() {

    private var mDataList : ArrayList<MenuItemInfo> = ArrayList<MenuItemInfo>()
    varmAdapter : Adapter<MenuViewHolder>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        fab.setOnClickListener { view ->
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show()
        }

        val toggle = ActionBarDrawerToggle(
                this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
        drawer_layout.addDrawerListener(toggle)
        toggle.syncState()

    ※ ここまでは修正なし

        //データ作成
        makeMenuInfo()

     //Adapterの作成とタップ後の処理
        val adapter = object : MenuItemAdapter(this,mDataList) {
            override fun onListClicked(view: View?, position: Int) {

                when (position) {
                    0 -> {
                        item_icon.setColorFilter(0xcc0000ff.toInt(), PorterDuff.Mode.SRC_IN)
                        message.text = "Share!!"
                    }
                    1 -> {
                        message.text = "gallery!!"
                    }
                    2 -> {
                        message.text = "send!!"
                    }
                    3 -> {

                        message.text = "camera!!"
                    }
                    4 -> {
                        message.text = "manage!!"
                    }
                    5 -> {
                        message.text = "slideshow!!"
                    }
                }
                drawer_layout.closeDrawer(GravityCompat.START)
            }
        }
        nav_recycler_view.adapter = adapter
        nav_recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
    }

    private fun makeMenuInfo() {
        nav_view.setBackgroundColor(Color.WHITE)
        mDataList.add(MenuItemInfo(0,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_share, null)!!,
                "share"))
        mDataList.add(MenuItemInfo(1,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_gallery, null)!!,
                "gallery"))
        mDataList.add(MenuItemInfo(2,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_send, null)!!,
                "send"))
        mDataList.add(MenuItemInfo(3,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_camera, null)!!,
                "camera"))
        mDataList.add(MenuItemInfo(4,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_manage, null)!!,
                "manage"))
        mDataList.add(MenuItemInfo(5,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_slideshow, null)!!,
                "slideshow"))
        mDataList.add(MenuItemInfo(6,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_share, null)!!,
                "share"))
        mDataList.add(MenuItemInfo(7,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_gallery, null)!!,
                "gallery"))
        mDataList.add(MenuItemInfo(8,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_send, null)!!,
                "send"))
        mDataList.add(MenuItemInfo(9,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_camera, null)!!,
                "camera"))
        mDataList.add(MenuItemInfo(10,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_manage, null)!!,"manage"))
        mDataList.add(MenuItemInfo(11,
                ResourcesCompat.getDrawable(getResources(), R.drawable.ic_menu_slideshow, null)!!,"slideshow"))
    }
 ※ 修正していない箇所は省略
}

処理の流れを補足していきます。

1.データ作成

メニューに表示するためのデータを作成しています。今回は12個分のメニューデータを生成しています。

2.Adapterの作成

Adapterの作成を行います。1で作ったデータを引数としてい渡しています。

3.リスナ登録とタップ後の処理を追加

前述のようにリスナ登録はViewHolederにて実装しています。

Activityでは、その検知後の処理は記述していきます。

あくまでAdapterはリストのデータとその表示が責任範囲と考えるべきで、画面制御周りの動きはActivityで対応していくべき、という判断のためです。

今回はメニューの表示順を固定してあるので、引数のpositionで分岐させて、表示するメッセージを決めています。

4.メニューのクローズ

最後にメニューをクローズする処理です。

drawer_layout.closeDrawer(GravityCompat.START)

以上がNavigationVewのカスタマイズ方法でした。

メニューを作っているというよりは、RecyclerViewの実装をしていた、という感じじゃないでしょうか?

Galleryから作ってみるとある程度動くものが出来上がるので、それが正解のように見える部分もあります。

簡易な方法で対応しているところも多いので、そこを自作したものに置き換えていくイメージがついていれば、特段難しいものではありません。

つまり、レイアウトを自作していくイメージがついてるかどうか、が肝ですね。

それさえあれば、変更していく方法も見出していくことができると思います。

イメージが付いたら、ガシガシとカスタマイズしてみてください!