Be simple

”当たり前”が誰かのためになる

RecyclerViewでのグリッド表示の実装方法について

はじめに

こんにちは。

今回は、RecyclerViewでのグリッド表示の実装方法について書き残しておきたいと思います。

Google Play Storeアプリを例にとると、次のような感じです。

f:id:rozkey59:20190422203002p:plain

Google Play ストア サンプル

サンプルでは3カラムの表示になっています。

2カラムも3カラムもさほど実装方法に違いがないので、実装方法について書き残していきます。

 

今回記してある内容

  1. RecyclerViewでのグリッド表示のやり方を作りながら理解する
  2. 2カラムと3カラムの実装方法について

 

対象読者

  • Android開発を多少やったことがある人
  • RecyclerViewでのグリッド表示をやったことがない人
  • Javaではなく、Kotlinでの実装を知りたい人

 

例によって、サンプルコードは以下のリポジトリにまとめてあるので、よかったら参照してください。

github.com

 

開発前準備から、 RecyclerViewでのグリッド表示のやり方を作りながら理解するまで前回の記事と内容が被っている部分がありますが、この記事だけ見ている場合を考えて最初から書いていきます。

 

では、実際にやっていきましょう。

 

開発前準備

開発をする前に、今回はKotlinとDataBindingを利用することと、androidXに対応した形で行なっています。最新の技術に合わせていく方針でやっていくため、対応していない方は対応した上で読み進めていただけますと幸いです。

 

DataBindingを利用するために、app配下のbuild.gradleに下記のように追加してください。...は中略を意味しており、gradleファイルに記載する必要はないです。以後も同様です。

 

android {
...
dataBinding {
enabled = true
}
...
}

 

androidXの対応に関しては、先人の知恵を借りて以下を参考にしてください。

qiita.com

新しく開発する場合には、下記の赤矢印の指しているチェックを入れるだけで対応は完了です。LanguageもKotlinにしてください。

f:id:rozkey59:20190422145356p:plain

新しく作成した時

これで、開発準備は完了です。

 

RecyclerViewでのグリッド表示のやり方を作りながら理解する

レイアウトを用意する

 まずはじめに、レイアウトを用意します。

今回は、RecyclerViewを表示するActivityをGridTwoColumnActivityとしているため、activity_grid_two_column.xmlというレイアウトファイルを用意します。

実際の実装は次のようになります。

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

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>

<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="36sp"
android:textStyle="bold"
android:text="Grid Two Column"
android:layout_marginStart="12dp"
/>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:clipToPadding="false"
/>

</LinearLayout>
</layout>

 

今回はTitleを別に用意していますが、ヘッダー付きRecyclerViewも作成できます。

 

次に、各々のセルのレイアウトについて別に用意しましょう。

セルのレイアウトは、item_gridxmlとして私は用意しました。名前は分かりやすければ、どんな名前でも構いません。

次のように書きます。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data>

<variable name="data"
type="www.rozkey59.tokyo.androiduitips.grid.GridListData"/>

</data>

<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardElevation="4dp"
>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5F5"
>

<ImageView
android:id="@+id/image"
android:layout_width="200dp"
android:layout_height="200dp"
android:scaleType="centerCrop"
tools:src="@drawable/iphone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>

<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.label}"
android:textStyle="bold"
android:textSize="12sp"
android:gravity="bottom"
android:paddingTop="2dp"
android:paddingEnd="8dp"
android:paddingBottom="2dp"
android:background="@drawable/background_label"
android:textColor="@android:color/white"
tools:text="Test"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>
</layout>

 

DataBindingを利用しているため<data>タグの中で、<variable>を定義し、nameにはこのリソースないで利用する変数名を、typeには表示したいデータのクラスのフルpathを記載します。

今回、ImageViewには、画像を入れ、TextViewには、ラベルの文字列を利用するためdataからそれぞれ入れています。

DataBindingでvariableタグに定義した変数をattributeに記載するときは、"@{name.hoge}"のようにし、nameはvariableタグにて宣言した名前を、hogeには自分が作成したDataClassの変数名を記載します。

 

今回、自分はグリッドに表示するデータのクラスをGridListData.ktとして次のように書いています。

data class GridListData(
val image: Int,
val label: String
)

hogeの部分はここで定義したimageなどを入れたいViewに対して記載します。

宣言したクラスのパスが間違っていたり、変数がなかったりするとビルド時にDataBindingのエラーが出るので、ここら辺を見直してみてください。

 

次は、Adapterについてです。

 

独自Adapterを用意する

RecyclerView.Adapterを継承した独自Adapterを用意します。今回、自分の場合は次のように実装しました。

class GridTwoAdapter(val dataList: List<GridListData>): RecyclerView.Adapter<GridTwoAdapter.GridTwoViewHolder>() {

lateinit var listener: OnItemClickListener
lateinit var imageListener: OnSetImageIdListener

override fun onBindViewHolder(holder: GridTwoViewHolder, position: Int) {
val data = dataList[position]
holder.binding.data = data
holder.binding.image.setImageResource(imageListener.onSetImageId())
holder.binding.container.setOnClickListener {
listener.onClick(it, position, data)
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GridTwoViewHolder {
setOnItemClickListener(listener)
setImageIdListener(imageListener)
return GridTwoViewHolder(
ItemGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}

override fun getItemCount(): Int {
return dataList.size
}

interface OnItemClickListener {
fun onClick(view: View, position: Int, data: GridListData)
}

interface OnSetImageIdListener {
fun onSetImageId(): Int
}

fun setOnItemClickListener(listener: OnItemClickListener) {
this.listener = listener
}

fun setImageIdListener(listener: OnSetImageIdListener) {
this.imageListener = listener
}

class GridTwoViewHolder(val binding: ItemGridBinding): RecyclerView.ViewHolder(binding.root)
}

 

RecyclerView.ViewHolderを継承した独自ViewHolderクラスを最初に作成しましょう。

class GridTwoViewHolder(val binding: ItemGridBinding): RecyclerView.ViewHolder(binding.root)

 bindingの方は、先ほど作成したitem_grid.xmlをlayoutタグでくくっているため、自動生成されたBindingクラスを指定しましょう。最初にビルドしないと参照できないと思いますので、ビルドしてから指定しましょう。

 

次に、RecyclerView.Adapterを継承したことで、次の3つの関数をoverrideして必要な処理を記載する必要があります。

  1. onCreateViewHolder
  2. onBindViewHolder
  3. getItemCount

それぞれについてみていきましょう。

 

onCreateViewHolder

新たに作成した独自ViewHolderをインスタンス化し作成します。

次のように指定して作成しましょう。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GridTwoViewHolder {
setOnItemClickListener(listener)
setImageIdListener(imageListener)
return GridTwoViewHolder(
ItemGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}

GridViewHolderは引数にDataBindigでinflateした値を渡します。

このときに、各セルをクリックした際の処理をもたせたいのでsetOnItemClickListenerでlistenerをセットしています。また、画像をランダムに表示したいため、setImageIdListenerでimageListenerをセットしています。

 

listenerはメンバにもたせて、interfaceを次のように定義します。

interface OnItemClickListener {
fun onClick(view: View, position: Int, data: GridListData)
}

interface OnSetImageIdListener {
fun onSetImageId(): Int
}

setter関数も作りましょう。

fun setOnItemClickListener(listener: OnItemClickListener) {
this.listener = listener
}

fun setImageIdListener(listener: OnSetImageIdListener) {
this.imageListener = listener
}

createViewHolderはこのように作成します。

 

onBindViewHolder

onBindViewHolderでは各セルの処理を書いたりします。

今回は次のようにしています。

override fun onBindViewHolder(holder: GridTwoViewHolder, position: Int) {
val data = dataList[position]
holder.binding.data = data
holder.binding.image.setImageResource(imageListener.onSetImageId())
holder.binding.container.setOnClickListener {
listener.onClick(it, position, data)
}
}

各positionのセルにdataをセットします。こうすることで、DataBindingで指定したViewに値が渡るようになります。

setImageResourceでimageListenerのonSetImageIdを呼ぶようにすることで、Activity側で指定した画像のIDを設定できるようになります。

setOnClickListenerではクリックしたときにlistenerのonClickを呼ぶようにしてあげます。こうすることでActivity側でクリック処理を記載することができるようになります。

getItemCount

Adapterにセットされるのアイテムのデータの総数です。

今回は、GridListDataのリストをAdapterの引数で渡しているので、これのサイズを指定します。実際に書くと次のようになります。

override fun getItemCount(): Int {
return dataList.size
}

 

ここまでで、独自Adapterの実装については終わりです。

次は、Activityの実装についてです。

 

Activityの実装

最初に作成したactivity_grid_two_column.xmlのRecyclerViewに先ほどまで作成してきたAdapterをセットし、作成して行きます。

全体の実装は次のとおりです。

class GridTwoColumnActivity: AppCompatActivity() {

private lateinit var binding: ActivityGridTwoColumnBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUp()
}

private fun setUp() {
binding = DataBindingUtil.setContentView(this, R.layout.activity_grid_two_column)
binding.apply {
val adapter = GridTwoAdapter(createDataList())
recyclerView.adapter = adapter
recyclerView.layoutManager = GridLayoutManager(
this@GridTwoColumnActivity, 2, GridLayoutManager.VERTICAL, false
)
recyclerView.addItemDecoration(
GridTwoItemDecoration(resources.getDimension(R.dimen.carousel_margin).toInt())
)
adapter.setOnItemClickListener(object : GridTwoAdapter.OnItemClickListener {
override fun onClick(view: View, position: Int, data: GridListData) {
Toast.makeText(this@GridTwoColumnActivity, data.label, Toast.LENGTH_SHORT).show()
}
})
adapter.setImageIdListener(object : GridTwoAdapter.OnSetImageIdListener{
override fun onSetImageId(): Int {
return getRandomImage()
}
})
}
}

private fun createDataList() : List<GridListData> {
val dataList = mutableListOf<GridListData>()
for(i in 0 until 20) {
dataList.add(
GridListData(
image = getRandomImage(),
label = "¥12$i"
)
)
}
return dataList
}

private fun getRandomImage(): Int {
val imageListArray = resources.obtainTypedArray(R.array.image_list)
val drawableIds = mutableListOf<Int>()
for (i in 0 until imageListArray.length()) {
drawableIds.add(imageListArray.getResourceId(i, 0))
}
imageListArray.recycle()
drawableIds.shuffle()
return drawableIds[0]
}

}

 

まず、Activityのレイアウトファイルを適用します。

メンバに次のようにbindingファイルを型にもつ変数を定義します。

private lateinit var binding: ActivityGridTwoColumnBinding

次に、onCreateのなかでDataBindingUtilを用いて、セットします。lazyで行っても構いません。

binding = DataBindingUtil.setContentView(this, R.layout.activity_grid_two_column)

今回はsetUp()という関数を作ってその中で行っています。

 

それが終わったら、先ほど作成した独自AdapterをRecyclerViewのAdapterにセットします。

binding.apply {
val adapter = GridTwoAdapter(createDataList())
recyclerView.adapter = adapter
}

この時に、Viewに渡すDataのリストを作成します。createDataListという関数でListを作成するようにしました。

private fun createDataList() : List<GridListData> {
val dataList = mutableListOf<GridListData>()
for(i in 0 until 20) {
dataList.add(
GridListData(
image = getRandomImage(),
label = "¥12$i"
)
)
}
return dataList
}

今回はAPIコールなどしていないので、自身で適当な長さのリストにDataを適当に作って当てはめました。

 

グリッド表示をするのに忘れてはいけないのが、LayoutManagerです。

今回はGridLayoutManagerを用意します。

binding.apply {
recyclerView.layoutManager = GridLayoutManager(
this@GridTwoColumnActivity, 2, GridLayoutManager.VERTICAL, false
)
}

GridLayoutManagerには引数が4つありますが、2つ目の数字はSpanCountを入れます。ここで指定した値がカラム数になります。今回は2カラムのサンプルを作成しているので2にしています。

3つめの引数はorientationでリストのスクロールの方向を指定します。今回は縦方向にしたいので、GridLayoutManager.VERTICALを指定しています。

 

ここまできてから実行すると、次のようになります。

f:id:rozkey59:20190422210440g:plain

GridSample (No Item Decoration)

これはこれでいいような気がしますが、マージンを開けたデザインを作りたい時がありますよね。

そういう時はItemDecorationを使用しましょう。

次のようにRecyclerViewに指定してあげます。

binding.apply {
recyclerView.addItemDecoration(
GridTwoItemDecoration(resources.getDimension(R.dimen.carousel_margin).toInt())
)
}

addItemDecorationでRecyclerView.ItemDecoration()を継承したカスタムクラスを指定します。

今回、自分はGridTwoItemDecorationとして次のようなカスタムクラスを作成しました。

class GridTwoItemDecoration(val margin: Int): RecyclerView.ItemDecoration() {

override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
with(outRect) {
top = margin / 2
left = margin / 2
right = margin / 2
bottom = margin / 2
}
}
}

marginを引数に渡し、getItemOffsetsという関数をoverrideしてきて上下左右のマージン半分にしています。半分にしなくとも16dp開けたければ8dpを指定するようにすれば2で割る必要は無くなりそうです。

これで、実行すると次のようになります。

f:id:rozkey59:20190422210836g:plain

Grid Sample (Use ItemDecoration)

マージンを入れたいだけならItemDecorationを利用して実装しちゃいましょう。

これで、グリッド表示の完成です。

 

次は、2カラムと3カラムの実装方法についてです

2カラムと3カラムの実装方法について

2カラムは先ほどのようにしていただいて、3カラムを実装する際には実は先ほどの実装を多少変えるだけで良いです。

変えるのは、GridLayoutManagerのspanCountの数ItemDecorationのみです。

変更する箇所の実装は次の通りです。

binding.apply {
recyclerView.layoutManager = GridLayoutManager(
this@GridThreeColumnActivity, 3, GridLayoutManager.VERTICAL, false
)
recyclerView.addItemDecoration(
GridThreeItemDecoration(resources.getDimension(R.dimen.carousel_margin).toInt())
)
}

3カラムにしたい時は、spanCountの数を 3にして、ItemDecorationを変更します。

今回は、GridThreeItemDecorationというRecyclerView.ItemDecoration()を継承したクラスを作成しました。実際の実装は次の通りです。

class GridThreeItemDecoration(private val margin: Int): RecyclerView.ItemDecoration() {

override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
when (parent.getChildAdapterPosition(view) % 3) {
0 -> {
with(outRect) {
top = margin / 2
left = margin
right = margin / 2
bottom = margin / 2
}
}
1 -> {
with(outRect) {
top = margin / 2
left = margin / 2
right = margin / 2
bottom = margin / 2
}
}
2 -> {
with(outRect) {
top = margin / 2
left = margin / 2
right = margin
bottom = margin / 2
}
}
}
}
}

parent.getChildAdapterPosition(view) % 3をwhenの条件に入れていますが、これは0が左、1が真ん中、2が右を意味しており、それぞれのmarginが半分にしたいとことそのままの値を使いたい部分とがあるためこの分岐を加えています。

これは、全てのマージンが均等に配置されることが良いという理由のため行なっています。

もし、マージンを個別に変えたい場合は、条件式やそれぞれのマージンを変える必要があります。

ここまできたものを実行すると次のようになります。

f:id:rozkey59:20190422211857g:plain

3カラムのサンプル

無事、3カラムの表示ができていることを確認できます。

サンプルはどこかのフリマアプリを参考に作ってみました。高さは適当ですが。

中身のレイアウトを変えれば、Google Playストアのレイアウトを作ることも可能です。 

 

おわりに

RecyclerViewでのグリッド表示に関してのTipsをまとめてみました。少しでも参考になれば幸いです。

 

説明がうまくできていない部分などあると思いますので、もし、不明点やもっとこうしたほうがいいなどあれば、コメントにて指摘などしていただけると嬉しいです。

 

補足などあれば、また補足として追加していきます。

 

今後も、不定期ではありますが、こういったTipsをまとめて発信していきたいと思います。

それでは、またの機会に。