RecyclerViewで複数のDividerを実装する
RecyclerViewでは、Divider(区切り)は自分で実装しなければなりません。
標準の線を引く方法はよく見つかるのですが、複数の線を引いたり、逆に線を引かなかったりするというのがあまり見つからないのでまとめました。
Support Libraryに追加されたDividerItemDecoration
確認したSupport Libraryのバージョンは 25.3.1
です。
標準の線は、Support Libraryの DividerItemDecoration
を使うことで引けます。内部の実装は、 mDivider
というDrawableを、Canvasに描画しています。
なので、描画をさせなければ線を引かない風にできますし、描画するDrawableを変えれば、異なる線を引くことができます。
基本的な流れ
描画領域の確保
区切り線の描画領域は ItemDecoration#getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
で、セルのxmlで定義したセルの大きさから、さらに余白をつけることができます。
なので、区切り線を描画させたくないときは、 outRect.set(0, 0, 0, 0)
とすべて0
にします。
下に引きたいときは、 outRect.set(0, 0, 0, mDivider.intrinsicHeight)
と下に余白を取ります(left, top, right, bottom)。
mDivider.intrinsicHeight
はDrawableの高さです。
セルごとの描画領域を求める
描画領域を確保したら、実際に描画します。
ItemDecoration#onDraw(Canvas c, RecyclerView parent, State state)
で描画していきます。画面を動かす度に呼ばれます。
Support LibraryのDividerItemDecoration
では、 drawVertical
を定義してそこで横の区切り線の処理をしています。
Math.round(ViewCompat.getTranslationY(child)
でセルの上部のY座標の求め、余白を加味したセルの高さを合わせて、区切り線を描画すべきY座標を求めます。
求めたY座標から、区切り線の高さを引くことで、区切り線を描画すべきRectがわかります。
求めたRectを、 mDivider
にセットし、Canvasに描画することで指定した領域に区切り線が表示されます。
- DividerItemDecoration.java(Support Library)
... parent.getDecoratedBoundsWithMargins(child, mBounds); final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child)); final int top = bottom - mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); ...
独自のDividerItemDecoration
Support Libraryの DividerItemDecoration
では、標準の区切り線か、 DividerItemDecoration#setDrawable(drawable: Drawable)
で設定した区切り線の1種類しか使えません。なので、 RecyclerView.ItemDecoration
を継承して独自の DividerItemDecoration
を作ります。
基本的な流れはSupport Libraryの DividerItemDecoration
と同じなのでソースを丸ごと拝借します。
必要なことは、 そのセルに区切り線が必要かどうか と考えます。 セルから区切り線を必要としているかどうかをわかるようにします。
RecyclerView#getChildViewHolder(View child)
で、セルのViewからViewHolderを取得できるので、ViewHolderに区切り線の有無の情報をもたせることにします。
特定のViewHolderに依存させない
Divider
というInterfaceを定義します。さらに、それを実装した NoDivider
と CustomDivider
を定義します。
CustomDivider
には区切り線の高さと、実際に描画するDrawableのリソースを持たせます。
このInterfaceをViewHolderが実装することで、 DividerItemDecoration
が特定のViewHolderに依存することを避けます。
- getItemOffsets
... val vh = parent.getChildViewHolder(view) when(vh) { is NoDivider -> outRect.set(0, 0, 0, 0) is CustomDivider -> outRect.set(0, 0, 0, vh.height) else -> outRect.set(0, 0, 0, mDivider.intrinsicHeight) } ...
Interfaceを実装していないViewHolderは、標準の区切り線を利用しています。
Drawableを使いまわす
CustomDivider
のリソースからDrawableを都度作成すると、パフォーマンスが悪くなるのでDividerItemDecoration
に保持させます。
... private val mDividerMap: HashMap<Divider, Drawable> = HashMap() ...
一度作成した Drawableは保持して保持して、使いまわすようにします。
- drawVertical
... val vh = parent.getChildViewHolder(child) when(vh) { is NoDivider -> {} is CustomDivider -> { val drawable = mDividerMap[vh]?: // Reuse divider ResourcesCompat.getDrawable(context.resources, vh.drawableRes, null)?.let { mDividerMap.put(vh, it) } val top = bottom - (vh.height + 1) // Line height < Bounds height drawable?.setBounds(left, top, right, bottom) drawable?.draw(canvas) } else -> { val top = bottom -mDivider.intrinsicHeight mDivider.setBounds(left, top, right, bottom) mDivider.draw(canvas) } } ...
区切り線の描画範囲が、Drawableより小さい場合は正しく描画されません。そのため、
val top = bottom - (vh.height + 1)
として区切り線の開始位置を調整しています。これは bottom
を求める際に Math.round(ViewCompat.getTranslationY(child)
を利用しているのと、リソースから高さを求めているので、そのあたりの丸め誤差によって生じているのだと考えています。
ViewHolderに実装する
DividerItemDecoration
を以上のように実装できれば、区切り線をつけたり消したりするのは ViewHolderを実装したクラスに、さらにDivider
を実装するだけで済むようになります。
- OddViewHolder.kt
class OddViewHolder(view: View): ViewHolder<Data>(view), CustomDivider { override val height = view.context.resources.getDimensionPixelSize(R.dimen.small_margin) override val drawableRes = R.drawable.dashed_line_divider ... }
- EvenViewHolder.kt
class EvenViewHolder(view: View): ViewHolder<Data>(view), NoDivider {
...
}
まとめ
一度作ってしまえば使いまわせるので便利です。 サンプルは横方向には対応していませんが、同じようにすることで対応できると思います。
ただし、ガイドライン的には、Divider
を多様をすることで本来の効果が薄れてしまう可能性があるとあるので使い所には注意が必要です。
サンプルはこちらです
参考
DividerItemDecoration / Android Developers
RecyclerView / Android Developer
RecyclerView.ItemDecorationについて #関モバ / Takuji->find;
How do I make a dotted/dashed line in Android? / Stack Overflow
How can a divider line be added in an Android RecyclerView? / Stack Overflow