RecyclerViewでListItemの位置に応じた角丸を設定する

RecyclerViewでリストを角丸にしたくなる場合があります。
安直にはRecyclerView全体を角丸にしてしまう方法もありますが、以下のように、リスト全体の上下左右以外も角丸にしたい場合、各要素の位置に応じて角丸を設定する必要があります。

|||
|--|--|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/596d3c94-e5bd-7684-ec88-deb15daca1df.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/cafc8381-8769-9cef-b5a4-3cf41e50975e.png)|

角丸の付け方はいろいろあると思いますが、制限はあるもののbackgroundに設定するdrawableを変更するのが簡単でしょうか。
GradientDrawableで角丸とマージン要素含めて定義しておいて、背景のdrawableを切り替えるだけで良いようにしてみます。
layer-listでitemにoffsetを設定し、shapeのpaddingに同じ値を設定することでそれっぽく定義できます。この場合、marginではなくpaddingなので要素サイズへの影響を考える必要があります。

```bg_top.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:end="8dp" android:start="8dp" android:top="8dp">
        <shape android:shape="rectangle">
            <solid android:color="@color/white" />
            <corners android:radius="16dp" />
            <padding android:end="8dp" android:start="8dp" android:top="8dp"/>
        </shape>
    </item>
</layer-list>
```

```bg_header.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:bottom="1dp" android:end="8dp" android:start="8dp" android:top="8dp">
        <shape android:shape="rectangle">
            <solid android:color="@color/white" />
            <corners android:topLeftRadius="16dp" android:topRightRadius="16dp"/>
            <padding android:bottom="1dp" android:end="8dp" android:start="8dp" android:top="8dp"/>
        </shape>
    </item>
</layer-list>
```

```bg_middle.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:bottom="1dp" android:end="8dp" android:start="8dp">
        <shape android:shape="rectangle">
            <solid android:color="@color/white" />
            <padding android:bottom="1dp" android:end="8dp" android:start="8dp"/>
        </shape>
    </item>
</layer-list>
```

```bg_footer.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:bottom="8dp" android:start="8dp" android:end="8dp">
        <shape android:shape="rectangle">
            <solid android:color="@color/white" />
            <corners android:bottomLeftRadius="16dp" android:bottomRightRadius="16dp"/>
            <padding android:bottom="8dp" android:start="8dp" android:end="8dp"/>
        </shape>
    </item>
</layer-list>
```

ってことで、onBindViewHolderでpositionに応じてbackgroundを変更します。

```kotlin
private class HogeAdapter(activity: Activity) : ListAdapter<Int, ItemViewHolder>(
    object : DiffUtil.ItemCallback<Int>() {
        override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean = oldItem == newItem
        override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean = oldItem == newItem
    }
) {
    private val inflater = activity.layoutInflater

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder =
        ItemViewHolder(ItemContentBinding.inflate(inflater, parent, false))

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.binding.text.text = "item: " + getItem(position)
        holder.binding.root.setBackgroundResource(
            when (position) {
                0 -> R.drawable.bg_top
                1 -> R.drawable.bg_header
                itemCount - 1 -> R.drawable.bg_footer
                else -> R.drawable.bg_middle
            }
        )
    }
}
```

しかし、要素を更新したとき、単に順序が変化しただけでは、`onBindViewHolder`がコールされないという問題があります。

|||
|--|--|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/596d3c94-e5bd-7684-ec88-deb15daca1df.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/8aa69fe7-83e1-df6c-d096-206999bca425.png)|

表示位置が変化したことを受け取れるコールバックはなさそうです。

# 対応方法

位置が変化したことを直接受け取ることはできないので、この背景要素もコンテンツの一部として、要素リストにパラメータを追加して、位置が変化し、背景を変更する必要がある場合に`onBindViewHolder`がコールされるようにします。

どの背景を指定するのかという情報を追加したdataクラスをつくって、

```kotlin
private enum class Background {
    TOP,
    HEADER,
    MIDDLE,
    FOOTER,
}
private data class Item(
    val background: Background,
    val id: Int,
)
```

リストの順序に応じてこの値を追加したものをAdapterにsubmitListします。

```kotlin
private fun makeItemList(): List<Item> =
    makeIdList().let { list ->
        list.mapIndexed { index, id ->
            Item(
                when (index) {
                    0 -> Background.TOP
                    1 -> Background.HEADER
                    list.lastIndex -> Background.FOOTER
                    else -> Background.MIDDLE
                }, id
            )
        }
    }
```

ListAdapterを以下のように実装し、DiffUtil.ItemCallbackで背景が変化した場合もコンテンツの変化として扱うようにします。

```kotlin
private class FugaAdapter(activity: Activity) : ListAdapter<Item, ItemViewHolder>(
    object : DiffUtil.ItemCallback<Item>() {
        override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean = oldItem.id == newItem.id
        override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean = oldItem == newItem
    }
) {
    private val inflater = activity.layoutInflater

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder =
        ItemViewHolder(ItemContentBinding.inflate(inflater, parent, false))

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.binding.text.text = "item: " + getItem(position).id
        holder.binding.root.setBackgroundResource(
            when (getItem(position).background) {
                Background.TOP -> R.drawable.bg_top
                Background.HEADER -> R.drawable.bg_header
                Background.FOOTER -> R.drawable.bg_footer
                Background.MIDDLE -> R.drawable.bg_middle
            }
        )
    }
}
```

これで、リスト内の要素がシャッフルされた場合も、ちゃんと背景が位置に応じて変化するようになります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/94602f9b-66c7-e9a1-f9bb-aa7aa68cb3fc.gif)

以上です。