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は省略しています)
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 を改造していきましょう。
まずは、コンストラクタで最初と最後のスキップ数を指定できるようにしましょう。
class VerticalDividerItemDecoration(
private val context: Context,
private val skipFirstLines: Int = 0,
private val skipLastLines: Int = 1,
) : RecyclerView.ItemDecoration() {
省略しないなら DividerItemDecoration を使うでしょうし、一番用途が多そうな末尾を描画しない指定をデフォルト値として設定しています。
次は、 getItemOffsets()
で、描画する範囲外は 0 を返すようにしましょう。
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()
ですが、これは描画ループの範囲指定を変更するだけでよいですね。
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 全体を載せておきます。
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()
}
}
以上です。