Square layout on GridLayoutManager for RecyclerView

43,437

Solution 1

To have the square elements in my RecyclerView, I provide a simple wrapper for my root View element; I use the following SquareRelativeLayout in place of 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);
    }

}

Then, in my XML layout for the adapter, I've just referenced the custom view as shown in the following. Though, you can do this programmatically also.

<?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>

Note: Depending on which orientation your grid is, then you may want to have the width based off of height (GridLayoutManager.HORIZONTAL) instead of the height being based off the width (GridLayoutManager.VERTICAL).

Solution 2

Constraint layout solves this problem. Use 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>

EDIT

Set ImageView width to 0dp. match_parent is now deprecated for ConstraintLayout.

Solution 3

In case someone would like to scale the view differently - this is how you do it:

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);
}

Solution 4

Starting API 26 (Support Library 26.0), one can use ConstraintLayout that exposes aspect ratio property to force views to be squared: 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
}

Example of layout I'm using in 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" is the key attribute here

Solution 5

A small update for ConstraintLayout for androidx.

Include this line to your build.gradle:

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'

I wanted to get a RecycleView with GridLayoutManager with square CardViews and I used such a layout for items:

<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"
        >

On the ConstraintLayout

  • layout_width="match_parent" is important to let the item fill as much space as RecyclerView provides
  • layout_height="wrap_content" do not let the item to fill all the height given by RecyclerView, but use the constrained height, provided by ConstraintLayout. In my case, when I used FrameLayout or LinearLayout, the items were "tall".

On the child node, in my case CardView

  • limiting size to zero is important: layout_width="0dp" and layout_height="0dp" it means, that width and height are contrained
  • layout_constraintDimensionRatio="H,1:1" makes the desired effect, by setting H you define that height is to be constrained 1:1 is the ratio.

See some detailed explanations on the offsite.

Share:
43,437
Paul Woitaschek
Author by

Paul Woitaschek

Updated on December 02, 2020

Comments

  • Paul Woitaschek
    Paul Woitaschek over 3 years

    I try to make a grid-layout with square images. I thought that it must be possible to manipulate the GridLayoutManager by manipulating onMeasure to do a

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

    instead of

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

    but unfortunately, that didn't work.

    Any ideas?

  • Nauman Afzaal
    Nauman Afzaal about 9 years
    Thanks alot for this help
  • Muhammad Naderi
    Muhammad Naderi almost 9 years
    awesome job, quick note though, there is no need to your footnote, since GridLayoutManager takes care of it
  • tccpg288
    tccpg288 almost 8 years
    Why do you use RelativeLayout? Wouldn't LinearLayout make more sense?
  • Anonsage
    Anonsage almost 8 years
    You should be able to use any root view that you want. The key is to override the onMeasure(...)
  • Mateus
    Mateus over 7 years
    Dear, not work for me... I don't know why... After update build tools, never more "square" items at GridLayoutManager.
  • wonsuc
    wonsuc about 7 years
    Work with Glide perfactly.
  • Pelanes
    Pelanes almost 7 years
    Worked, best solution to maintain code short and clean. Thanks
  • Mavamaarten
    Mavamaarten almost 7 years
    Thank you for this additional bit of info. Multiplying the widthMeasureSpec by the desired aspect ratio seemed logical, but obviously did not work.
  • Teffi
    Teffi almost 7 years
    @andryr I agree with you. I had to set mine to 0dp width also. Probably its due to some recent updates with constraint layout.
  • Ryan Amaral
    Ryan Amaral almost 7 years
    If you need a full visible square in landscape mode, you can do that by getting the minimum value (from width and height) and set that value to both sizes. Example: onMeasure(...) { int size = Math.min(widthMeasureSpec, heightMeasureSpec); super.onMeasure(size, size); }
  • jpoppe
    jpoppe over 6 years
    As of release 26.0.0, the Percent Support library is deprecated. Clients of this module should migrate to the new ConstraintLayout widget, which is provided as a separate artifact in SDK Manager.
  • Sven
    Sven over 5 years
    Works with app:layout_constraintDimensionRatio="W, 1:1"
  • blueware
    blueware over 5 years
    This should be the accepted answer as we need the most updates solution. Current accepted answer needs more boilerplate code to be written.
  • hkchakladar
    hkchakladar about 5 years
    best solutions.
  • Yamashiro Rion
    Yamashiro Rion almost 5 years
    Thank you, the solution is simple and elegant!
  • Sergei Buvaka
    Sergei Buvaka over 4 years
    this mast be approved answer
  • Leonardo Sibela
    Leonardo Sibela over 4 years
    The constructor targeting the lollipop api did not work for me. It couldn't find the constructor.