如何设置 RecyclerView 项目相对于父项的最大高度

How to set RecyclerView item max height relative to parent

按照这个问题中的建议,我几乎已经完成了这项工作

Android percent screen width in RecyclerView item

但是,这会为所有视图设置一个高度,无论内容如何,​​高度都相同。在我的例子中,我只想限制 RecyclerView 中的项目与其高度相关的高度。

在我的房车中,我有一些高度不同的物品,其中一些物品甚至比房车还高。

我的目标是告诉该项目最多可以达到 RecyclerView 高度的 60%,到目前为止,我发现实现此目的的唯一方法是每当有新项目时手动设置最大高度使用此逻辑绑定到视图持有者:

constraintSet.constrainMaxHeight(viewId, (int) (recyclerView.getMeasuredHeight() * 0.6));

其中 recyclerView 是对托管 RecyclerView 的引用,我使用适配器的方法 onAttachedToRecyclerView 传递给适配器,viewId 是主视图RV 项目 ConstraintLayout 根视图,我用它来控制它可以占用的最大高度。

虽然这可以满足我的需要,但我想为此找到一个 simpler/more 优化的解决方案,在简单性方面类似于这个:

这意味着 ViewTreeObserver 的任何使用都被排除在外。

有什么想法吗?

覆盖 measureChildWithMargins 是可行的方法,但是要使其实际工作,您需要自定义 LayoutParams 以便您可以从适配器“继承”数据或直接膨胀您想要的百分比XML.

这个布局管理器会处理它:

open class PercentLinearLayoutManager : LinearLayoutManager {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)

    // where we actually force view to be measured based on custom layout params
    override fun measureChildWithMargins(child: View, widthUsed: Int, heightUsed: Int) {
        val pp = child.layoutParams as PercentParams
        if (pp.maxPercentHeight <= 0.0f)
            super.measureChildWithMargins(child, widthUsed, heightUsed)
        else {
            val widthSpec = getChildMeasureSpec(width, widthMode,
                    paddingLeft + paddingRight + pp.leftMargin + pp.rightMargin + widthUsed, pp.width,
                    canScrollHorizontally())
            val maxHeight = (height * pp.maxPercentHeight).toInt()
            val heightSpec = when (pp.height) {
                ViewGroup.LayoutParams.MATCH_PARENT -> View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.EXACTLY)
                ViewGroup.LayoutParams.WRAP_CONTENT -> View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.AT_MOST)
                else -> View.MeasureSpec.makeMeasureSpec(min(pp.height, maxHeight), View.MeasureSpec.AT_MOST)
            }
            child.measure(widthSpec, heightSpec)
        }
    }

    // everything below is needed to generate custom params
    override fun checkLayoutParams(lp: RecyclerView.LayoutParams) = lp is PercentParams

    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        return PercentParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
    }

    override fun generateLayoutParams(lp: ViewGroup.LayoutParams): RecyclerView.LayoutParams {
        return PercentParams(lp)
    }

    override fun generateLayoutParams(c: Context, attrs: AttributeSet): RecyclerView.LayoutParams {
        return PercentParams(c, attrs)
    }

    class PercentParams : RecyclerView.LayoutParams {
        /** Max percent height of recyclerview this item can have. If height is `match_parent` this size is enforced. */
        var maxPercentHeight = 0.0f

        constructor(c: Context, attrs: AttributeSet) : super(c, attrs){
            val t = c.obtainStyledAttributes(attrs, R.styleable.PercentLinearLayoutManager_Layout)
            maxPercentHeight = t.getFloat(R.styleable.PercentLinearLayoutManager_Layout_maxPercentHeight, 0f)
            t.recycle()
        }
        constructor(width: Int, height: Int) : super(width, height)
        constructor(source: MarginLayoutParams?) : super(source)
        constructor(source: ViewGroup.LayoutParams?) : super(source)
        constructor(source: RecyclerView.LayoutParams?) : super(source)
    }
}

要处理来自 XML 的 inflation,需要自定义属性,将其添加到 values/attrs.xml:

<declare-styleable name="PercentLinearLayoutManager_Layout">
    <attr name="maxPercentHeight" format="float"/>
</declare-styleable>

那么你有两个选择:

1 - 更改 onBindViewHolder 内的自定义参数以控制每个项目:

val lp = holder.itemView.layoutParams as PercentLinearLayoutManager.PercentParams
if(modifyThisViewHolderHeight) {
    lp.maxPercentHeight = 0.6f
} else {
    lp.maxPercentHeight = 0.0f // clear percentage in case viewholder is reused
}

2 - 在您的 viewholder 布局中使用自定义属性:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:maxPercentHeight ="0.6">

    <!-- rest of layout-->
</FrameLayout>

我推荐我在链接答案中使用的相同近似策略:在创建 ViewHolder 时设置最大高度。但是,通用 View 不支持 maxHeight 属性,因此我们将利用 ConstraintLayout 来实现我们想要的。

如果您的项目视图的根视图已经是 ConstraintLayout,那太好了,否则您可以将其中包含的任何内容包装起来。在这个人为的例子中,我使用了这个布局(FrameLayout 在 XML 中使用 0dp 宽度和 wrap_content 高度:

<androidx.constraintlayout.widget.ConstraintLayout ...>
    <FrameLayout ...>
        <TextView .../>
    </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

在创建时,我将 FrameLayout 的最大高度设置为父级高度的 60%:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    val inflater = LayoutInflater.from(parent.context)
    val itemView = inflater.inflate(R.layout.recycler_item, parent, false)
    val holder = MyViewHolder(itemView)

    val params = holder.frame.layoutParams as ConstraintLayout.LayoutParams
    params.matchConstraintMaxHeight = (parent.height * 0.6).toInt()
    holder.frame.layoutParams = params

    return holder
}