炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

RecyclerView で Section を表示する


アクトインディ Advent Calendar 2016 2日目の記事になります。2015年の11月にアクトインディに入社して 1 年が経ちました。 入社してからは iOS アプリの開発に携わらせてもらい、現在は鋭意 Android アプリの開発をしています。

子どもとのお出かけ先に困っている方は、是非いこーよアプリを使ってみてください ^^

iko-yo.net

アドベントカレンダーの本題ですが、今回は Android の RecyclerView で Section をどうやって表現するかの話を書きたいと思います。アプリの設定画面など、表示する項目が決まっていて、セクションで区切られたリストを表示したいケースがあると思います。iOS では TableView に Section が用意されているので容易に実装できますが Android では List にセクションといった要素がない為、セクション用のレイアウトとリストアイテム用のレイアウトファイルを項目で切り替えて実装する必要が出てきます。今回は Enum を使った実装を紹介したいと思います。実装のサンプルは Kotlin 1.0.5 になります。

f:id:t-namikata:20161201184934p:plain

やっていることは簡単で ヘッダーには setOnClickListener を設定せず、リストアイテムの場合に setOnClickListener を設定するといった事をしています。

1. リストで表示する Enum クラスを作成する

enum class ListItem(val title: String) {
    HeaderFruits(title = "果物"),
    Apple(title = "りんご"),
    Orange(title = "オレンジ"),
    HeaderVegetables(title = "野菜"),
    Carrot(title = "人参"),
    Onion(title = "玉ねぎ"),
    HeaderDrinks(title = "飲み物"),
    Milk(title = "牛乳"),
    Water(title = "水")
}

2. Adapter を作成する

Adapter の雛形は以下になります。コメントアウトの箇所をそれぞれ実装していきたいと思います。

class ItemsAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private val items = ListItem.values()

    override fun getItemViewType(position: Int): Int {
        // 1. ヘッダーかリストアイテムかで異なる viewType を返すように実装する
    }

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
        // 2. viewType によって読み込む layout ファイルを指定する
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
        // 3. text のセットとアイテムクリック時のイベントをセットする
    }

    override fun getItemCount(): Int {
        return this.items.count()
    }
}
1. ヘッダーかリストアイテムかで異なる viewType を返すように実装する

ヘッダー用かリストアイテム用のレイアウトを返すか判別する為の viewType を返却するように設定します。

class ItemsAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private val items = ListItem.values()
    private val headerType = 0
    private val itemType = 1

    override fun getItemViewType(position: Int): Int {
        val item = this.items[position]
        when (item) {
            ListItem.HeaderFruits, ListItem.HeaderVegetables, ListItem.HeaderDrinks -> return headerType
            else -> return itemType
        }
    }
2. viewType によって読み込む layout ファイルを指定する

設定した viewTyep を利用してヘッダー用とリストアイテム用の Header を指定した ViewHolder を返却するようにします。

class ItemsAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    ... 省略 ...

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(context)
        when (viewType) {
            headerType -> return ViewHolderItem(inflater.inflate(ViewHolderItem.headerLayoutId, parent, false))
            itemType -> return ViewHolderItem(inflater.inflate(ViewHolderItem.itemLayoutId, parent, false))
            else -> return ViewHolderItem(inflater.inflate(ViewHolderItem.itemLayoutId, parent, false))
        }
    }

    ... 省略 ...

    class ViewHolderItem(view: View) : RecyclerView.ViewHolder(view) {
        companion object {
            val headerLayoutId = R.layout.header
            val itemLayoutId = R.layout.item
        }
    }
3. text のセットとアイテムクリック時のイベントをセットする

あとは Enum で定義した各アイテム毎に行いたいアクションを定義します。

class ItemsAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    ... 省略 ...

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
        holder ?: return
        val item = this.items[position]
        val textView = holder.itemView.findViewById(R.id.item_text_view) as TextView
        textView.text = item.title

        holder.itemView.setOnClickListener {
            this.action(item = item)
        }
    }

    ... 省略 ...

    private fun action(item: ListItem) {
        when (item) {
            ListItem.Apple -> {
                Toast.makeText(context, "りんご", Toast.LENGTH_LONG).show()
            }
            ListItem.Orange -> {
                Toast.makeText(context, "オレンジ", Toast.LENGTH_SHORT).show()
            }
            ListItem.Carrot -> {
                Toast.makeText(context, "人参", Toast.LENGTH_SHORT).show()
            }

        // ...

            else -> {}
        }
    }

adapter をセットする

最後に activity で adapter をセットします。

class MainActivity : AppCompatActivity() {
    private val recyclerView: RecyclerView by bindView(R.id.recycler_view)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        this.recyclerView.setHasFixedSize(true)
        this.recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

        val settingAdapter = ItemsAdapter(context = this)
        recyclerView.adapter = settingAdapter
    }
}

Enum で定義した順番がリストに表示される順番という気持ち悪さはありますが、ある程度はアイテムが増減しても、そこまで改修が大きくなることはない実装かと思います。各レイアウトファイルは以下のような感じです。

activity_main.xml

<?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/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="test.sectionrecyclerview.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

item.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">

    <TextView
        android:id="@+id/item_text_view"
        android:padding="16dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</FrameLayout>

header.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">

    <TextView
        android:background="@android:color/darker_gray"
        android:id="@+id/item_text_view"
        android:padding="16dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</FrameLayout>