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

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

```kotlin
binding.recyclerView.addItemDecoration(
    DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
```

|dividerなし|DividerItemDecoration|
|--|--|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/7dabc475-7b4d-e1dd-dc03-6e24ebeb78a2.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/35469306-ce6a-3119-e507-5bd516798536.png)|

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

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

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

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.java

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

# VERTICAL 限定 DividerItemDecoration

というわけで、 [VerticalDividerItemDecoration.java](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.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行省略|
|--|--|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/84dfc3be-b03d-fa0a-8bf0-4d2df4c45f45.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/b581e987-c543-c5aa-1539-1bdc67f1746f.png)|

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()
    }
}
```

以上です。