具有自动适应跨度计数的 StaggeredGridLayoutManager

StaggeredGridLayoutManager with auto-fit span count

我需要一个符合以下两个要求的 StaggeredGridLayoutManager:

直到现在,我还没有交错网格项的需要,所以我使用了 GridLayoutManager 的扩展来提供此功能:

import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Recycler
import kotlin.math.max
import kotlin.math.min

class GridAutoFitLayoutManager : GridLayoutManager {
    private var mColumnWidth = 0
    private var mMaximumColumns: Int
    private var mLastCalculatedWidth = -1

    @JvmOverloads
    constructor(
        context: Context?,
        columnWidthDp: Int,
        maxColumns: Int = 99
    ) : super(context, 1) //Initially set spanCount to 1, will be changed automatically later.
    {
        mMaximumColumns = maxColumns
        setColumnWidth(columnWidthDp)
    }

    private fun setColumnWidth(newColumnWidth: Int) {
        if (newColumnWidth > 0 && newColumnWidth != mColumnWidth) {
            mColumnWidth = newColumnWidth
        }
    }

    override fun onLayoutChildren(
        recycler: Recycler,
        state: RecyclerView.State
    ) {
        val width = width
        val height = height
        if (width != mLastCalculatedWidth && mColumnWidth > 0 && width > 0 && height > 0) {
            val totalSpace: Int = if (orientation == RecyclerView.VERTICAL) {
                width - paddingRight - paddingLeft
            } else {
                height - paddingTop - paddingBottom
            }
            val spanCount = min(
                mMaximumColumns,
                max(1, totalSpace / mColumnWidth)
            )
            setSpanCount(spanCount)
            mLastCalculatedWidth = width
        }
        super.onLayoutChildren(recycler, state)
    }
}

在回收站视图中使用布局管理器:

recyclerView.layoutManager = GridAutoFitLayoutManager(context, resources.getDimension(R.dimen.grid_column_width).toInt())

最后,项目视图布局的宽度设置为 match_parent,因此它们将占据布局管理器允许的 space 大小。

但是,我无法让它与 StaggeredGridLayoutManager 一起使用。这是我的代码:

import android.content.Context
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Recycler
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import kotlin.math.max
import kotlin.math.min

class GridAutoFitStaggeredLayoutManager : StaggeredGridLayoutManager {
    private var mColumnWidth = 0
    private var mMaximumColumns: Int
    private var mLastCalculatedWidth = -1

    @JvmOverloads
    constructor(
        context: Context?,
        columnWidthDp: Int,
        maxColumns: Int = 99
    ) : super(1, LinearLayout.VERTICAL) //Initially set spanCount to 1, will be changed automatically later.
    {
        mMaximumColumns = maxColumns
        setColumnWidth(columnWidthDp)
    }

    private fun setColumnWidth(newColumnWidth: Int) {
        if (newColumnWidth > 0 && newColumnWidth != mColumnWidth) {
            mColumnWidth = newColumnWidth
        }
    }

    override fun onLayoutChildren(
        recycler: Recycler,
        state: RecyclerView.State
    ) {
        val width = width
        val height = height
        if (width != mLastCalculatedWidth && mColumnWidth > 0 && width > 0 && height > 0) {
            val totalSpace: Int = if (orientation == RecyclerView.VERTICAL) {
                width - paddingRight - paddingLeft
            } else {
                height - paddingTop - paddingBottom
            }
            val spanCount = min(
                mMaximumColumns,
                max(1, totalSpace / mColumnWidth)
            )
            setSpanCount(spanCount)
            mLastCalculatedWidth = width
        }
        super.onLayoutChildren(recycler, state)
    }
}

回收器视图一加载,我就得到以下异常:

java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling

问题是因为我在解析布局时调用了 setSpanCount(),这不是 GridLayoutManager 的问题,但在 StaggeredGridLayoutManager 中是不允许的。我的问题是,如果我不能在 onLayoutChildren() 期间设置跨度计数,我什么时候可以设置它?

我知道我可以在初始化布局管理器之前计算适合的列数,并将跨度计数传递给 StaggeredGridLayoutManager 构造函数。但是,这不会对方向变化做出反应,除非我向 activity 添加额外的逻辑来重新创建布局管理器,我希望避免这种情况。有没有办法让我的 GridAutoFitStaggeredLayoutManager 计算跨度计数 自行处理方向变化?

仔细查看了异常的原因,这应该可以防止错误:

import android.content.Context
import android.os.Handler
import android.util.DisplayMetrics
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Recycler
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import kotlin.math.max
import kotlin.math.min

class GridAutoFitStaggeredLayoutManager : StaggeredGridLayoutManager {

    companion object {
        private fun setInitialSpanCount(context: Context?, columnWidthDp: Int) : Int {
            val displayMetrics: DisplayMetrics? = context?.resources?.displayMetrics
            return ((displayMetrics?.widthPixels ?: columnWidthDp) / columnWidthDp)
        }
    }

    private var mContext: Context?
    private var mColumnWidth = 0
    private var mMaximumColumns: Int
    private var mLastCalculatedWidth = -1

    @JvmOverloads
    constructor(
        context: Context?,
        columnWidthDp: Int,
        maxColumns: Int = 99
    ) : super(setInitialSpanCount(context, columnWidthDp), VERTICAL)
    {
        mContext = context
        mMaximumColumns = maxColumns
        mColumnWidth = columnWidthDp
    }

    override fun onLayoutChildren(
        recycler: Recycler,
        state: RecyclerView.State
    ) {
        if (width != mLastCalculatedWidth && width > 0) {
            recalculateSpanCount()
        }
        super.onLayoutChildren(recycler, state)
    }

    private fun recalculateSpanCount() {
        val totalSpace: Int = if (orientation == RecyclerView.VERTICAL) {
            width - paddingRight - paddingLeft
        } else {
            height - paddingTop - paddingBottom
        }
        val newSpanCount = min(
            mMaximumColumns,
            max(1, totalSpace / mColumnWidth)
        )
        queueSetSpanCountUpdate(newSpanCount)
        mLastCalculatedWidth = width
    }

    private fun queueSetSpanCountUpdate(newSpanCount: Int) {
        if(mContext != null) {
            Handler(mContext!!.mainLooper).post { spanCount = newSpanCount }
        }
    }
}