RecyclerView.LayoutManagerとRecyclerView.ItemDecoration

近況ですが11月にFRESH LIVEからCATSに異動しました。新規事業の立ち上げをしまくるAndroid/iOS/Webのエンジニア集団です。
2ヶ月が経ちましたがほぼずっとLayoutManagerを書いてます。最近2個めのLayoutManagerのPRを投げました。

前回の記事 RecyclerView.LayoutManagerの実装方法では触れなかったItemDecorationとLayoutManagerのItemDecoration対応について書きたいと思います。

RecyclerView.ItemDecoration

RecyclerViewには各childViewに装飾をしたりoffsetを設けるためにRecyclerView.ItemDecorationというクラスが提供されています。
用途に合わせて以下3つの関数をoverrideして実装します。

  • getItemOffsets
  • onDraw
  • onDrawOver

getItemOffsets

各childViewのleft, top, right, bottomにoffsetを設けることができます。
引数のoutRectにpxを渡すことでoffsetを設定できます。

childViewのbottomに10dpのoffsetを設定してみましょう。

recyclerView.addItemDecoration(Decoration())
class Decoration : RecyclerView.ItemDecoration() {

  override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
    super.getItemOffsets(outRect, view, parent, state)
    outRect.bottom = 10.dp
  }
}

itemdecoration offset screenshot

RecyclerView#getChildCount, RecyclerView#getChildAdapterPositionなどを用いれば最後のchildViewにはoffsetを付けないといったことも可能です。

onDraw, onDrawOver

RecyclerViewのcanvasに対して直接描き込むことでchildViewを装飾する事ができます。
onDrawはchildViewが描画される前に、onDrawOverはchildViewが描画された後にcanvasに書き込む事ができます。childViewに被せてなにかを描き込みたい場合はonDrawOverを使うといいでしょう。
この2つの関数はスクロールの度に実行されるので重い処理は避けるようにすべきです。

childViewのbottomに10dpのoffsetを設定した上で、そこに10dpのdividerを描画してみましょう。

class Decoration : RecyclerView.ItemDecoration() {
  
  private val paint = Paint().apply { color = Color.BLACK }
  
  override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
    super.getItemOffsets(outRect, view, parent, state)
    outRect.bottom = 10.dp
  }

  override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDraw(c, parent, state)
    (0 until parent.childCount).forEach {
      val v = parent.getChildAt(it)
      c.drawRect(Rect(parent.left, v.bottom, parent.right, parent.bottom + 10.dp), paint)
    }
  }
}

itemdecoration divider screenshot

LayoutManagerとItemDecoration

ItemDecorationがoffsetの設定を行っていても、LayoutManager側がそのoffsetを考慮してViewを配置していなければoffsetは反映されません。
LayoutManagerを実装する際にItemDecorationにも対応するには以下の関数を知っておくと良いです。
ItemDecorationに対応したLayoutManagerを実装するには、これらの関数だけを使ってchildViewの配置をしていればItemDecoration対応は問題ありません。

  • measureChild, measureChildWithMargins
  • getDecoratedMeasuredWidth, getDecoratedMeasuredHeight
  • layoutDecorated, layoutDecoratedWithMargins
  • getDecoratedLeft, getDecoratedTop, getDecoratedRight, getDecoratedBottom

measureChild, measureChildWithMargins

childViewのmeasureを行ってくれる関数です。その際に、RecyclerViewにaddsあれているItemDecorationから、そのchildViewに対するoffsetを取得してoffsetを考慮したmeasureを行ってくれます。
measureChildWithMarginsはchildViewに設定されているmarginも考慮してくれます。

getDecoratedMeasuredWidth, getDecoratedMeasuredHeight

ItemDecorationによるoffsetを含んだ、childViewのwidth/heightを取得する関数です。
onLayoutChildrenなどでchildViewを配置していく際には、この関数をつかって次に配置するchildViewの位置を計算しましょう。

layoutDecorated, layoutDecoratedWithMargins

ItemDecorationによるoffsetを考慮してlayoutを行ってくれます。

getDecoratedLeft, getDecoratedTop, getDecoratedRight, getDecoratedBottom

ItemDecorationによるoffsetを加味したviewのleft, top, right, bottomを反してくれます。

最近作ってるLayoutManager

最近は仕事でも個人的にもLayoutManagerで遊んでいる。仕事で作ったものはそのうち公開するかもしれない(しないかもしれない)。個人的に作ってるものはそのうち公開する。 ついでに最近のLayoutManagerツイートを貼っておく。

Moyuru Aizawa 🐈 on Twitter

先月はこんな感じの、左側にHeader的なものを付けれるLinearLayoutみたいなLayoutManagerも作ったりした。 https://t.co/M1fcm9zTT4