RecyclerView中GridLayoutManager的正方形布局

75
我尝试使用正方形图像创建网格布局。我认为通过操纵onMeasure可以操纵GridLayoutManager以实现此目的。

我尝试使用正方形图像创建网格布局。我认为通过操纵onMeasure可以操纵GridLayoutManager以实现此目的。

super.onMeasure(recycler, state, widthSpec, widthSpec); 

代替

super.onMeasure(recycler, state, widthSpec, heightSpec);

但不幸的是,那没有起作用。

有任何想法吗?


你在重写 onMeasure 上是正确的,但不应该在 LayoutManager 中这样做,而是应该在 onCreateViewHolder 中创建的 View 中进行。 - pskink
9个回答

157
为了在我的RecyclerView中拥有正方形元素,我为根View元素提供了一个简单的封装器;我使用以下SquareRelativeLayout代替RelativeLayout
package net.simplyadvanced.widget;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;

/** A RelativeLayout that will always be square -- same width and height,
 * where the height is based off the width. */
public class SquareRelativeLayout extends RelativeLayout {

    public SquareRelativeLayout(Context context) {
        super(context);
    }

    public SquareRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SquareRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(VERSION_CODES.LOLLIPOP)
    public SquareRelativeLayout(Context context, AttributeSet attrs,         int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Set a square layout.
        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
    }

}

然后,在适配器的XML布局中,我仅引用了自定义视图,如下所示。不过,你也可以通过编程实现这一点。

<?xml version="1.0" encoding="utf-8"?>
<net.simplyadvanced.widget.SquareRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/elementRootView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <!-- More widgets here. -->

</net.simplyadvanced.widget.SquareRelativeLayout>

注意:根据您的网格方向,您可能希望基于高度(GridLayoutManager.HORIZONTAL)而不是基于宽度来设置宽度。

同时,您也可以基于宽度来设置高度(GridLayoutManager.VERTICAL)。


非常感谢您的帮助。 - Nauman Afzaal
4
太棒了!不过需要注意的是,你的脚注其实是不必要的,因为GridLayoutManager会自动处理它。 - Muhammad Naderi
2
你应该能够使用任何你想要的根视图。关键是要重写 onMeasure(...) 方法。 - Anonsage
完美地使用Glide。 - wonsuc
如果您需要在横屏模式下显示一个完整的正方形,可以通过获取宽度和高度的最小值,并将该值设置为两个尺寸来实现。例如:onMeasure(...) { int size = Math.min(widthMeasureSpec, heightMeasureSpec); super.onMeasure(size, size); } - Ryan Amaral
显示剩余2条评论

121

约束布局可以解决这个问题。使用app:layout_constraintDimensionRatio="H,1:1"

recyclerview_grid_layout.xml

<android.support.constraint.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ImageView
        android:id="@+id/imageview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="H,1:1"
        android:scaleType="centerCrop"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

</android.support.constraint.ConstraintLayout>

编辑

将ImageView的宽度设为0dp。在ConstraintLayout中,match_parent已被弃用。


亲爱的,对我不起作用...我不知道为什么...更新构建工具后,GridLayoutManager中再也没有“方形”项目了。 - Mateus
工作得很好,保持代码简洁清晰的最佳解决方案。谢谢。 - Pelanes
@andryr 我同意你的观点。我也不得不将我的宽度设置为0dp。可能是由于最近的约束布局更新所致。 - Teffi
这应该是被接受的答案,因为我们需要最新的解决方案。当前被接受的答案需要编写更多的样板代码。 - blueware
谢谢,解决方案简单而优雅! - Yamashiro Rion
显示剩余3条评论

20

如果有人想以不同比例缩放视图 - 这是您需要做的:

private static final double WIDTH_RATIO = 3;
private static final double HEIGHT_RATIO = 4;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = (int) (HEIGHT_RATIO / WIDTH_RATIO * widthSize);
    int newHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
    super.onMeasure(widthMeasureSpec, newHeightSpec);
}

感谢您提供的额外信息。将widthMeasureSpec乘以期望的纵横比似乎是合理的,但显然行不通。 - Mavamaarten

9

从API 26(支持库26.0)开始,可以使用ConstraintLayout来强制视图成为正方形,该布局公开了纵横比属性: https://developer.android.com/training/constraint-layout/index.htm

android {
    compileSdkVersion 26
    buildToolsVersion '26.0.2'
    ...
}
...
dependencies {
    compile 'com.android.support:appcompat-v7:26.0.2'
    compile 'com.android.support.constraint:constraint-layout:1.1.0-beta1' //use whatever version is current
}

我正在使用GridLayoutManager布局的示例:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/margin_small"
    android:background="@drawable/border_gray"
    android:gravity="center">

    <android.support.constraint.ConstraintLayout
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="h,1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <!-- place your content here -->


    </android.support.constraint.ConstraintLayout>

</android.support.constraint.ConstraintLayout>

这里的关键属性是app:layout_constraintDimensionRatio="h,1:1"

8
一则针对 androidx 中 ConstraintLayout 的小型更新。
在你的 build.gradle 文件中加入以下这行代码:
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'

我想使用GridLayoutManager创建一个具有正方形CardViews的RecycleView,我使用了以下布局作为项目:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/tools"
    android:layout_width="match_parent"  
    android:layout_height="wrap_content"
    android:padding="8dp"
    >

    <androidx.cardview.widget.CardView
        android:id="@+id/cardView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        card_view:cardElevation="4dp"
        app:layout_constraintDimensionRatio="H,1:1"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        >

关于 ConstraintLayout

  • layout_width="match_parent" 很重要,让项目填满 RecyclerView 提供的所有空间
  • layout_height="wrap_content" 不允许项目填满 RecyclerView 给定的所有高度,而是使用 ConstraintLayout 提供的约束高度。在我的情况下,当我使用 FrameLayout 或 LinearLayout 时,项目很“高”。

关于子节点,在我的情况下是 CardView

  • 将大小限制为零很重要:layout_width="0dp" 和 layout_height="0dp",这意味着宽度和高度被约束
  • layout_constraintDimensionRatio="H,1:1" 通过设置 H 来定义约束高度,1:1 是比例,实现期望的效果。

请参阅此处以获得更详细的解释。


3
请尝试使用这个FrameLayout的扩展。它执行双重测量以提高一致性。它还支持自定义XML属性,以从布局设置所需的纵横比。
public class StableAspectFrameLayout extends FrameLayout {

    private int aspectWidth = 1;
    private int aspectHeight = 1;

    public StableAspectFrameLayout(Context context) {
        this(context, null, 0);
    }

    public StableAspectFrameLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StableAspectFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        extractCustomAttrs(context, attrs);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public StableAspectFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        extractCustomAttrs(context, attrs);
    }

    private void extractCustomAttrs(Context context, AttributeSet attrs) {
        if (attrs == null) return;
        TypedArray a = context.getResources().obtainAttributes(attrs, R.styleable.StableAspectFrameLayout);
        try {
            aspectWidth = a.getInteger(R.styleable.StableAspectFrameLayout_aspect_width, 1);
            aspectHeight = a.getInteger(R.styleable.StableAspectFrameLayout_aspect_height, 1);
        } finally {
            a.recycle();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int newSpecWidth = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
        int newH = Math.round(((float) getMeasuredWidth()) * aspectHeight / aspectWidth);
        int newSpecHeigh = MeasureSpec.makeMeasureSpec(newH, MeasureSpec.EXACTLY);
        super.onMeasure(newSpecWidth, newSpecHeigh);
    }
}

同时,还需要翻译attrs.xml文件中的内容。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--  StableAspectFrameLayout  -->
    <declare-styleable name="StableAspectFrameLayout">
        <attr name="aspect_width" format="integer"/>
        <attr name="aspect_height" format="integer"/>
    </declare-styleable>

</resources>

1

我曾经遇到过类似的问题,需要填充一个在RecyclerView网格中呈正方形的视图。以下是我的解决方法。

在onCreateViewHolder方法中,我使用了ViewTreeObserver和GlobalLayoutListener来获取布局的宽度。该布局的宽度属性为match_parent。而我的RecyclerView在水平方向上居中对齐。

final View view = LayoutInflater.from(mActivity).inflate(R.layout.list_item_deals, parent, false);
    view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            int side = view.getMeasuredWidth();

            ViewGroup.LayoutParams lp = view.getLayoutParams();
            lp.width = side;
            lp.height = side;
            view.setLayoutParams(lp);
        }
    });

参考图像


1

我不喜欢所选答案,让我提供我的解决方案:不要将整个项目布局包装在SomeDammyLayoutWithFixedAspectRatio中,而是可以通过修改GridLayoutManager中measureChild方法的代码来实现。我已经替换了以下这些行:

if (mOrientation == VERTICAL) {
        wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
                horizontalInsets, lp.width, false);
        hSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(),
                verticalInsets, lp.height, true);
    } else {
        hSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
                verticalInsets, lp.height, false);
        wSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getWidthMode(),
                horizontalInsets, lp.width, true);
    }

目标:

if (mOrientation == VERTICAL) {
        wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
                horizontalInsets, lp.width, false);
        hSpec = wSpec;
    } else {
        hSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
                verticalInsets, lp.height, false);
        wSpec = hSpec;
    }

看起来它运行良好。

不要误会,这也相当混乱,但至少这个解决方案不会通过扩展视图层次结构来损害应用程序性能。


1

再次推荐使用相对新的“percent”布局。使用依赖项'com.android.support:percent:25.2.0',你可以像这样做:

<android.support.percent.PercentFrameLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content">
      <ImageView
         android:id="@+id/image"
         app:layout_widthPercent="100%"
         app:layout_aspectRatio="100%"
         android:padding="10dp"
         android:scaleType="centerCrop"
         android:cropToPadding="true"
         tools:background="#efdbed"
         />
   </android.support.percent.PercentFrameLayout>

它可能比ConstraintLayout快得多,尽管有一天我们可能不再关心。

从版本26.0.0开始,百分比支持库已被弃用。该模块的客户端应迁移到新的ConstraintLayout小部件,该小部件作为SDK Manager中的单独构件提供。 - jpoppe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接