androidandroid-recyclerviewgridlayoutmanagerepoxy-modelview

Dynamically Change Span count and ModelView Size for Recycler View


I have en epoxy recycler view, that can display an item in the maximum possible size and also want to display the maximum number of items by adjusting span count and item size. For that, I am using this layout manager,

class GridAutoFitLayoutManager(
    context: Context,
    private val minItemSize: Int,
    private val maxItemSize: Int
) : GridLayoutManager(context, 1) {

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        updateSpanCount()
        super.onLayoutChildren(recycler, state)
    }

    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        val layoutParams = super.generateDefaultLayoutParams()
        val itemSize = calculateItemSize()
        layoutParams.width = itemSize
        layoutParams.height = itemSize
        return layoutParams
    }

    override fun generateLayoutParams(c: Context, attrs: android.util.AttributeSet): RecyclerView.LayoutParams {
        val layoutParams = super.generateLayoutParams(c, attrs)
        val itemSize = calculateItemSize()
        layoutParams.width = itemSize
        layoutParams.height = itemSize
        return layoutParams
    }

    private fun updateSpanCount() {
        val availableWidth = width - paddingRight - paddingLeft
        val availableHeight = height - paddingTop - paddingBottom
        val spanCount = calculateSpanCount(availableWidth, availableHeight)
        if (spanCount == 0) {
            setSpanCount(1)
        } else {
            setSpanCount(spanCount)
        }
    }

    private fun calculateSpanCount(availableWidth: Int, availableHeight: Int): Int {
        val itemCount = itemCount
        val minSpanCount = 1 // Minimum span count
        val maxSpanCount = (availableWidth / minItemSize).coerceAtMost(itemCount) // Maximum span count

        var bestSpanCount = maxSpanCount
        var bestItemSize = 0
        val spacing = 20
        for (spanCount in minSpanCount..maxSpanCount)  {
            val itemSize = calculateItemSizeWithSpan(spanCount)
            if (itemSize in minItemSize..maxItemSize) {
                val rowCount = availableHeight / itemSize
                val availableWidthAfterSpacing = availableWidth - ((spanCount -1) * spacing)
                val totalItemWidth = spanCount * itemSize
                //&& (rowCount * spanCount) <= itemCount
                if (totalItemWidth <= availableWidthAfterSpacing) {
                    if (itemCount <= (rowCount * spanCount)) {
                        bestSpanCount = spanCount
                        bestItemSize = itemSize
                        break
                    }

                }
            }
        }
        return bestSpanCount
    }


    private fun calculateItemSizeWithSpan(spanCount: Int): Int {
        val availableWidth = width - paddingRight - paddingLeft

        val spacing = 20

        val totalSpacing = spacing * (spanCount - 1)
        val totalItemWidth = availableWidth - totalSpacing

        return totalItemWidth / spanCount
    }

    private fun calculateItemSize(): Int {
        val availableWidth = width - paddingRight - paddingLeft
        val spanCount = spanCount
        val spacing = 20

        val totalSpacing = spacing * (spanCount - 1)
        val totalItemWidth = availableWidth - totalSpacing

        return totalItemWidth / spanCount
    }
}

This is calculating the sizes correctly. however, I am facing an issue. In the first load, there are fewer items say 8, so it displays an item of size 540 X 540.

enter image description here

Now when I switch to a different category, it is having 40 items, so the best possible size is calculated as 240. In this case, the item is not drawn correctly.

enter image description here

Any idea why this is not drawn correctly? Is this a moel view reuse issue ?


Solution

  • I suggest adding an override of checkLayoutParams method:

    override fun checkLayoutParams(lp : RecyclerView.LayoutParams) : Boolean {
       return lp.width == calculateItemSize()
    }
    

    This will discard layout params of reused viewholders when their size is not okay and create valid params to replace them.