読者です 読者をやめる 読者になる 読者になる

メモ2ブログ

メモtoウェブログ。旧ブログはこちら。 http://sakebook.blogspot.jp/

RecyclerViewで複数のDividerを実装する

RecyclerViewでは、Divider(区切り)は自分で実装しなければなりません。

標準の線を引く方法はよく見つかるのですが、複数の線を引いたり、逆に線を引かなかったりするというのがあまり見つからないのでまとめました。

f:id:sakebook:20170423181700g:plain

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を定義します。さらに、それを実装した NoDividerCustomDivider を定義します。 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を多様をすることで本来の効果が薄れてしまう可能性があるとあるので使い所には注意が必要です。

サンプルはこちらです

github.com

参考

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

ハードウェアアクセラレーションが有効だとstrokeでの点線描写が上手く描写出来ない / みんからきりまで

Dividers / Material design guidelines