Androidで複数ViewHolder, SpanSizeしたときのItemDecoration


ViewTypeとSpanSizeと余白

RecyclerViewはViewTypeとSpanSizeを組み合わせることで、複数のViewHolderを各々の大きさで表示することができます(ViewHolderやAdapterは複雑になりますが)

そのとき表示するViewの余白(縦横の両端と間の余白)を揃えるために、ViewHolderごとにMarginをつけてpositionによって表示/非表示を切り替えるのではなく、RecyclerView.ItemDecorationで余白をつけてみました

ItemDecorationによる余白

GridDecorationLayoutManager.kt
/**
 * 1,2,3カラムの [RecyclerView.ViewHolder] を表示する [GridLayoutManager]
 */
class GridDecorationLayoutManager(context: Context, val adapter: GridItemAdapter)
    : GridLayoutManager(context, 6, GridLayout.VERTICAL, false) {

    init {
        spanSizeLookup = object : SpanSizeLookup() {
            override fun getSpanSize(position: Int): Int {
                // 1カラム→6, 2カラム→3, 3カラム→2
                return adapter.getSpanSize(position)
            }
        }
    }
}
GridItemDecoration.kt
/**
 * [RecyclerView.ViewHolder] とSpanSizeによって余白を分ける [RecyclerView.ItemDecoration]
 */
class GridItemDecoration(context: Context) : RecyclerView.ItemDecoration() {

    private val margin = context.resources.getDimensionPixelSize(R.dimen.grid_decoration_margin)

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val params = view.layoutParams as? GridLayoutManager.LayoutParams
        val manager = parent.layoutManager as? GridLayoutManager
        val spanIndex = params?.spanIndex ?: 0
        val spanSize = params?.spanSize ?: 1
        val spanCount = manager?.spanCount ?: 1

        val isTop = parent.getChildAdapterPosition(view) == 0
        val isStart = spanIndex == 0
        val isEnd = spanIndex + spanSize == spanCount

        when (parent.findContainingViewHolder(view)) {
            is LargeViewHolder -> {
                // 1カラムで表示するViewHolder、最上部のとき上方向に余白をつける
                outRect.set(margin, if (isTop) margin else 0, margin, margin)
            }
            is MediumViewHolder -> {
                // 2カラムで表示するViewHolder、最上部のとき上方向に余白をつける、端かどうかで余白を変える
                outRect.set(
                        if (isStart) margin else margin / 2,
                        if (isTop) margin else 0,
                        if (isEnd) margin else margin / 2,
                        margin)
            }
            is SmallViewHolder -> {
                // 3カラムで表示するViewHolder、最上部のとき上方向に余白をつける、端かどうかで余白を変える
                outRect.set(
                        if (isStart) margin else if (isEnd) margin / 3 else margin * 2 / 3,
                        if (isTop) margin else 0,
                        if (isStart) margin / 3 else if (isEnd) margin else margin * 2 / 3,
                        margin)
            }
        }
    }
}

両端かどうかで余白を変える必要あるの?

上記のItemDecorationでは端(縦方向のGridLayoutManagerの場合 2~3カラムの一番左と一番右)のViewHolderで左右の余白を変えていますが、調べると出てくる実装例では左右の余白を左端のときだけ変えているものがありました
実装例を参考に左端のみ左右に余白をつけ左端以外は右側に余白をつけると、以下の様にViewHolderの表示サイズが異なってしまい、どうやらViewHolderごとに同じ大きさの余白とする必要がありそうでした


https://stackoverflow.com/questions/30524599/items-are-not-the-same-width-when-using-recyclerview-gridlayoutmanager-to-make-c

まとめ

  • ItemDecorationで余白をつけることはできた
  • 余白を同じにしないとViewHolderの表示サイズが変わってしまうのつらい、どうしよう
  • ItemDecorationとViewHolderでそれぞれ余白つけるのもあり?

Recycleってなんでしょうね