GridLayout
はリストを格子状に並べるという形式です
以前RecyclerViewについての記事を投稿した際にLayoutManagerにGridLayoutManager
というパターンがあるというのを記載しています
今回はGridLayoutManagerについて少し深掘りしようと思います
全てのセルが同じViewHolderで構成されているパターン
まずは全てのセルが同じViewHolderで構成されるパターンです
基本的にLinerLayoutManagerと作り方は同じでAdapterを作成してRecyclerViewと紐付けをします
ただ、GridLayoutManagerは紐付け方が異なるのでそこにフォーカスを当てて説明します
Adapterやセル(アイテム)のレイアウトは他のリスト表示の時と変わらないので下記まとめています
class HomeAdapter(private val countList: List<Int>): RecyclerView.Adapter<HomeAdapter.CountListRecyclerViewHolder>() { private lateinit var listener: HomeAdapterListener override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeAdapter.CountListRecyclerViewHolder { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(R.layout.grid_list, parent, false) return CountListRecyclerViewHolder(view) } override fun getItemCount(): Int { return countList.size } override fun onBindViewHolder(holder: CountListRecyclerViewHolder, position: Int) { holder.countText.text = countList[position].toString() holder.itemView.setOnClickListener { listener.contentTapped(position) } } class CountListRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var countText: TextView = itemView.findViewById(R.id.gridTextView) } fun setListener(listener: HomeAdapterListener) { this.listener = listener } interface HomeAdapterListener { fun contentTapped(position: Int) } }
<?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" app:cardCornerRadius="6dp" app:cardElevation="6dp" app:contentPadding="8dp" app:cardUseCompatPadding="true"> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <View android:id="@+id/image_view" android:layout_width="match_parent" android:layout_height="160dp"/> <TextView android:id="@+id/gridTextView" android:textSize="20sp" android:textColor="@android:color/black" android:textAlignment="center" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout> </androidx.cardview.widget.CardView>
異なるのはLayoutManagerです。格子状に並べるためにはGridLayoutManager
を使用します
val countList = listOf(1, 2, 3, 4, 5, 6, 7, 8) val adapter = HomeAdapter(countList) homeRecyclerView.adapter = adapter homeRecyclerView.layoutManager = GridLayoutManager( requireContext(), 2, RecyclerView.VERTICAL, false ) adapter.setListener(object: HomeAdapter.HomeAdapterListener { override fun contentTapped(position: Int) { // セルをタップした時の処理 } })
今回のGridLayoutManagerで横に2つずつ並べるようなリスト表示の設定を行っています(GridLayoutManagerの初期化の引数の2つ目でspanCount
を指定します)
ビルドしてみると次のような表示になります
セルのマージンを調整する
GridLayoutは基本的にはセル間のマージンがありません。マージンを追加する場合には例えばセルの右側に10dpマージンを追加するなどの方法が考えられます
しかし、その方法だと全てのセルに同じマージンが追加されることになります。特定のセルのみマージンを変更したい場合には別の方法で対応する必要があります
今回はItemDecoration
を使う方法で対応します
ItemDecorationクラスはアイテムの装飾を行います。例えば、区切り線や今回のようにマージンを追加したりできます
ItemDecorationを継承したカスタムクラスを作成して、RecyclerViewに追加します
class CustomGridItemDecoration(context: Context) : RecyclerView.ItemDecoration() { private val margin = 120 override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val params = view.layoutParams as? GridLayoutManager.LayoutParams val manager = parent.layoutManager as? GridLayoutManager val spanIndex = params?.spanIndex ?: 0 val spanSize = params?.spanSize ?: 1 val spanCount = manager?.spanCount ?: 1 // セルのindex val itemPosition = parent.getChildAdapterPosition(view) // セルのindexが2番目以下 val isTop = itemPosition < 2 val isStart = spanIndex == 0 val isEnd = spanIndex + spanSize == spanCount // 今回はviewHolderに応じて処理を行っているが、1つのviewHolderで1つのクラスを作成しても問題ない when (parent.findContainingViewHolder(view)) { is HomeAdapter.CountListRecyclerViewHolder -> { // outRect.set(leftのマージン, topのマージン, rightのマージン, bottomのマージン) outRect.set(0, if (isTop) margin else 0, 0, 0) } } } }
今回は0番目と1番目の上部に120のマージンを表示させるようにしました(getItemOffsets
はアイテムの上下左右に空間を設ける処理を行います)
ItemDecorationを継承したカスタムクラスを作成したら次のようにRecyclerViewに追加します
homeRecyclerView.addItemDecoration(CustomGridItemDecoration(requireContext()))
これで次のように特定のセルにマージンを追加できます
1列に表示させる数を変更する
先程までは1列に2つ表示させるという規則的なリストを作成しました
ただ、1列目は1つ、2列目は2つというように不規則に表示させたい場合もあると思います
ここでは不規則に表示させるパターンについて説明しようと思います
1列に表示させる数を設定するためにはGridLayoutManagerクラスのSpanSizeLookup
を使います。
SpanSizeLookupのgetSpanSize
で列に何個表示させるかを指定します
val layoutManager = GridLayoutManager(requireContext(), 2).apply { spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { // 2番目のセルは2つ分のサイズを使用して、それ以外は1つ分(半分)のサイズを使用する if (position == 2) { return 2 } return 1 } } } homeRecyclerView.layoutManager = layoutManager
今回の場合にはGridLayoutManager(requireContext(), 2)
で1列に2つ表示するように設定した上で2番目は2つ表示させるサイズを1つのセルに使い、
それ以外は1個分のサイズつまり画面の半分のサイズでセルを表示させるようにspanSizeLookupを設定しています
ビルドすると次のようになります
こうすることで不規則な個数を表示させることができますが、if (position == 2)
のようにpositionを固定できない場合にはどうしますか?むしろ、固定できない場合が多いと思います
そのような場合にはViewType
で区別することができます
まずAdapterでは他のAdapterとは重複しないViewTypeを指定します。 Int型を指定しなければいけないのでレイアウトIDを指定することが多い気がしますが、重複しなければいいです
override fun getItemViewType(position: Int): Int { return R.layout.grid_list }
そしてadapterのViewTypeを元にしてアイテムのサイズを指定します。 この場合には条件分岐をposition固定にしなくていい反面、Adapterを複数作る必要があります
val layoutManager = GridLayoutManager(requireContext(), 2).apply { spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return when (adapter.getItemViewType(position)) { R.layout.grid_list -> 1 } } } }
ItemDecorationの補足
ここで少しItemDecorationについて補足します。ItemDecorationの大まかな概要は次のようになります
class CustomGridItemDecoration(context: Context) : RecyclerView.ItemDecoration() { // アイテムの上下左右へ空白(透明)を設ける override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { } // アイテムが描画される前に描画されるため、RecyclerViewの下に装飾を表示 override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDraw(c, parent, state) } // アイテムが描画された後に描画されるため、RecyclerViewの上に装飾を表示 override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDrawOver(c, parent, state) } }
onDrawOver
とonDraw
は次のイメージです
少し前のItemDecorationの解説でで区切り線を追加したりするという説明をしたと思います
区切り線を入れたい場合は、DividerItemDecoration
という既成のItemDecorationがAPIに用意されています
val deco = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) homeRecyclerView.addItemDecoration(deco)
区切り線をカスタマイズしたい場合やRecyclerViewをカスタマイズしたい場合には、getItemOffsetでサイズを指定してonDrawもしくはonDrawOver(あるいは両方)で描画処理を行うというということを 意識して実装しましょう!