RecyclerView用GridLayoutManager wrap_content时item被拉伸如何解决
GridLayoutManager默认的测量逻辑是:先遍历当前行所有item计算各自内容高度,取最大值作为整行高度,之后会强制将同排所有item的高度设置为这个最大值,保证行对齐。这个逻辑是Grid布局的默认特性,和你写wrap_content没有冲突,你之前遇到的「加底部约束item被拉伸、删底部约束布局错位」,本质都是这个强制拉伸规则导致的。
你不需要换StaggeredGridLayoutManager,后者是瀑布流逻辑,纵向不对齐、容易出现滑动跳动,确实不适合信息卡片类场景,按下面两步改就能实现你要的效果。
第一步:修正item布局的层级配置
你现在item布局有两个容易触发拉伸的问题,先改掉:
CardView内部嵌套的ConstraintLayout当前高度写的是match_parent,当外层被强制设置为整行高度时,这个属性会让内部所有控件跟着撑满,直接把内容拉开。把这个属性改成android:layout_height="wrap_content"即可。- 你给
CardView写了固定宽度177dp、负边距(layout_marginEnd="-18sdp"、layout_marginBottom="-7dp"),这类硬编码值在不同屏幕尺寸下很容易出现列宽错位、边距异常,建议把CardView宽度改成match_parent,所有负边距改成正数值,让LayoutManager自动计算列宽,适配性更好。 - 给
CardView加上android:gravity="top"属性,保证内部内容始终靠上显示,不会因为父容器高度变化居中或者靠下。 - 最后一个分类文本的
app:layout_constraintBottom_toBottomOf="parent"约束保留,不用删除,保证内部约束链完整不会错位。
修改后的item根布局核心代码参考:
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="@dimen/_7sdp" android:layout_marginTop="@dimen/_14sdp" android:layout_marginEnd="@dimen/_7sdp" android:layout_marginBottom="@dimen/_7sdp" android:gravity="top" android:clickable="true" android:focusable="true" android:foreground="?android:attr/selectableItemBackground" app:cardCornerRadius="14dp" app:cardElevation="1dp"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <!-- 内部所有子控件原有约束保持不变,不需要修改 --> ... </androidx.constraintlayout.widget.ConstraintLayout> </androidx.cardview.widget.CardView>
记得把XML里RecyclerView标签下原有的app:layoutManager和app:spanCount属性删掉,后续在代码里设置LayoutManager即可。
第二步:替换LayoutManager,关闭同排item强制拉伸
如果改完布局还是有卡片被拉伸的情况(不同系统版本RecyclerView实现略有差异),就自定义一个GridLayoutManager,重写子View测量逻辑,让item只按自身内容计算高度,不强制匹配整行最高值,同时保留Grid布局横向对齐、按行排布的特性:
Kotlin版本代码:
class NonStretchGridLayoutManager( context: Context, spanCount: Int ) : GridLayoutManager(context, spanCount) { override fun measureChildWithMargins(child: View, widthUsed: Int, heightUsed: Int) { val lp = child.layoutParams as RecyclerView.LayoutParams // 宽度按Grid规则正常计算,保证列对齐 val widthSpec = getChildMeasureSpec( width, widthMode, paddingLeft + paddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, canScrollHorizontally() ) // 高度不强制匹配行高,交给子View自己按wrap_content计算 val heightSpec = View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED ) child.measure(widthSpec, heightSpec) } }
Java版本代码:
public class NonStretchGridLayoutManager extends GridLayoutManager { public NonStretchGridLayoutManager(Context context, int spanCount) { super(context, spanCount); } @Override public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) { RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); // 宽度按Grid规则正常计算,保证列对齐 int widthSpec = getChildMeasureSpec( getWidth(), getWidthMode(), getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, canScrollHorizontally() ); // 高度不强制匹配行高,交给子View自己按wrap_content计算 int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); child.measure(widthSpec, heightSpec); } }
之后在代码里给RecyclerView设置这个自定义LayoutManager即可:
rv_explore_buyer_outlet.layoutManager = NonStretchGridLayoutManager(requireContext(), 2)
修改后完全保留GridLayout的样式:两列横向严格对齐、按行排布不会出现瀑布流的错位跳动问题,同排item各自按内容自适应高度,较矮的item不会被拉伸,卡片底部约束正常不会出现内容错位,和设计稿效果完全一致。
内容的提问来源于stack exchange,提问作者Villa

