2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RecyclerViewのdividerを最初・最後に描画させない方法

Posted at

RecyclerView では、 ItemDecoration を追加することで、dividerを表示させることができます。 DividerItemDecoration という ItemDecorationが 用意されており、これを使うことで簡単に実装できますね。

binding.recyclerView.addItemDecoration(
    DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
dividerなし DividerItemDecoration

概ねこれで良いのですが、背景などとの兼ね合いから最後のアイテムの下にはdividerは表示したくないという要望は非常に良くあると思います。
先頭いくつ目までは表示したくないみたいな場合もあるかもしれません。

非常に良くある要望だとは思いますが、残念ながらDividerItemDecorationにはそのような要望を実現する機能はありません。ItemDecorationを自作する必要があります。

DividerItemDecorationには縦横どちらでも引数を変更することで使えるようになってはいますが、その分岐を含めても非常にコンパクトな実装になっています。

DividerItemDecoration.VERTICAL の場合に限定してしまえばさらにシンプルに実装できそうですよね。

VERTICAL 限定 DividerItemDecoration

というわけで、 VerticalDividerItemDecoration.java を VERTICAL 限定にして Kotlin で書き直すと以下のようになります。(drawableのsetter/getterは省略しています)

VerticalDividerItemDecoration.kt
class VerticalDividerItemDecoration(
    context: Context
) : RecyclerView.ItemDecoration() {
    private val bounds: Rect = Rect()
    private var divider: Drawable? = null

    init {
        context.obtainStyledAttributes(
            intArrayOf(android.R.attr.listDivider)
        ).use {
            divider = it.getDrawable(0)
        }
    }

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val divider = divider ?: run {
            outRect.setEmpty()
            return
        }
        outRect.set(0, 0, 0, divider.intrinsicHeight)
    }

    override fun onDraw(
        canvas: Canvas,
        parent: RecyclerView,
        state: RecyclerView.State,
    ) {
        val divider = divider ?: return
        canvas.save()
        val left: Int
        val right: Int
        if (parent.clipToPadding) {
            left = parent.paddingLeft
            right = parent.width - parent.paddingRight
            val top = parent.paddingTop
            val bottom = parent.height - parent.paddingBottom
            canvas.clipRect(left, top, right, bottom)
        } else {
            left = 0
            right = parent.width
        }
        for (i in 0..<parent.childCount) {
            val child = parent.getChildAt(i)
            parent.getDecoratedBoundsWithMargins(child, bounds)
            val bottom = bounds.bottom + child.translationY.roundToInt()
            val top = bottom - divider.intrinsicHeight
            divider.setBounds(left, top, right, bottom)
            divider.draw(canvas)
        }
        canvas.restore()
    }
}

これを改造して、最初/最後の描画をスキップできるようにしましょう。
overrideしているメソッドは2つ

getItemOffsets() は decoration が使用するサイズを返すメソッドです。描画しない場合は outRect にすべて0を書き込めばよいです。描画エリアが確保されなくなります。

onDraw() は decoration の描画を行うメソッドです。見ての通り、 recyclerView のすべての child に対して描画処理を行っています。不要な箇所のスキップが必要です。

最初/最後の描画をスキップさせる

ということで、 VerticalDividerItemDecoration を改造していきましょう。
まずは、コンストラクタで最初と最後のスキップ数を指定できるようにしましょう。

VerticalDividerItemDecoration.kt
class VerticalDividerItemDecoration(
    private val context: Context,
    private val skipFirstLines: Int = 0,
    private val skipLastLines: Int = 1,
) : RecyclerView.ItemDecoration() {

省略しないなら DividerItemDecoration を使うでしょうし、一番用途が多そうな末尾を描画しない指定をデフォルト値として設定しています。
次は、 getItemOffsets() で、描画する範囲外は 0 を返すようにしましょう。

VerticalDividerItemDecoration.kt
override fun getItemOffsets(
    outRect: Rect,
    view: View,
    parent: RecyclerView,
    state: RecyclerView.State
) {
    val divider = divider ?: run {
        outRect.setEmpty()
        return
    }
    val position = parent.getChildAdapterPosition(view)
    if (position in skipFirstLines..<state.itemCount - skipLastLines) {
        outRect.set(0, 0, 0, divider.intrinsicHeight)
    } else {
        outRect.setEmpty()
    }
}

引数に View と RecyclerView があるので getChildAdapterPosition() を使ってそのViewの位置を取得できます。また、 RecyclerView.State から要素の総数も取得できるので、これらから範囲外の場合は setEmpty() ですべて0を設定します。
最後に onDraw() ですが、これは描画ループの範囲指定を変更するだけでよいですね。

VerticalDividerItemDecoration.kt
override fun onDraw(
    canvas: Canvas,
    parent: RecyclerView,
    state: RecyclerView.State,
) {
    val divider = divider ?: return
    canvas.save()
    val left: Int
    val right: Int
    if (parent.clipToPadding) {
        left = parent.paddingLeft
        right = parent.width - parent.paddingRight
        val top = parent.paddingTop
        val bottom = parent.height - parent.paddingBottom
        canvas.clipRect(left, top, right, bottom)
    } else {
        left = 0
        right = parent.width
    }
    for (i in skipFirstLines..<parent.childCount - skipLastLines) {
        val child = parent.getChildAt(i)
        parent.getDecoratedBoundsWithMargins(child, bounds)
        val bottom = bounds.bottom + child.translationY.roundToInt()
        val top = bottom - divider.intrinsicHeight
        divider.setBounds(left, top, right, bottom)
        divider.draw(canvas)
    }
    canvas.restore()
}

こうすることで、 先頭末尾の任意の行数にdividerを描画しない指示ができるItemDecorationを作ることができます。

末尾省略 先頭2行省略

divider drawable を変更できるようにしておくと良いのでそれらを加えて
VerticalDividerItemDecoration.kt 全体を載せておきます。

VerticalDividerItemDecoration.kt
class VerticalDividerItemDecoration(
    private val context: Context,
    private val skipFirstLines: Int = 0,
    private val skipLastLines: Int = 1,
) : RecyclerView.ItemDecoration() {
    private val bounds: Rect = Rect()
    private var divider: Drawable? = null

    init {
        context.obtainStyledAttributes(
            intArrayOf(android.R.attr.listDivider)
        ).use {
            divider = it.getDrawable(0)
        }
    }

    fun setDrawable(drawable: Drawable?) {
        divider = drawable
    }

    fun setDrawable(drawableRes: Int) {
        divider = AppCompatResources.getDrawable(context, drawableRes)
    }

    fun getDrawable(): Drawable? = divider

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State,
    ) {
        val divider = divider ?: run {
            outRect.setEmpty()
            return
        }
        val position = parent.getChildAdapterPosition(view)
        if (position in skipFirstLines..<state.itemCount - skipLastLines) {
            outRect.set(0, 0, 0, divider.intrinsicHeight)
        } else {
            outRect.setEmpty()
        }
    }

    override fun onDraw(
        canvas: Canvas,
        parent: RecyclerView,
        state: RecyclerView.State,
    ) {
        val divider = divider ?: return
        canvas.save()
        val left: Int
        val right: Int
        if (parent.clipToPadding) {
            left = parent.paddingLeft
            right = parent.width - parent.paddingRight
            val top = parent.paddingTop
            val bottom = parent.height - parent.paddingBottom
            canvas.clipRect(left, top, right, bottom)
        } else {
            left = 0
            right = parent.width
        }
        for (i in skipFirstLines..<parent.childCount - skipLastLines) {
            val child = parent.getChildAt(i)
            parent.getDecoratedBoundsWithMargins(child, bounds)
            val bottom = bounds.bottom + child.translationY.roundToInt()
            val top = bottom - divider.intrinsicHeight
            divider.setBounds(left, top, right, bottom)
            divider.draw(canvas)
        }
        canvas.restore()
    }
}

以上です。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?