GridLayoutManager - how to auto fit columns?
Asked Answered
M

8

80

I have a RecyclerView with a GridLayoutManager that displays Card Views. I want the cards to rearrange according to the screen size (the Google Play app does this kind of thing with its app cards). Here is an example:

enter image description here

enter image description here

Here is how my app looks at the moment:

enter image description here

enter image description here

As you can see the cards just stretch and don't fit the empty space that is made from the orientation change. So how can I do this?

Code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Json;
using System.Threading;
using System.Threading.Tasks;
using Android.Media;
using Android.App;
using Android.Support.V4.App;
using Android.Support.V4.Content.Res;
using Android.Support.V4.Widget;
using Android.Support.V7.Widget;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Util;
using Android.Views;
using Android.Widget;
using Android.Net;
using Android.Views.Animations;
using Android.Graphics;
using Android.Graphics.Drawables;
using Newtonsoft.Json;
using *******.Adapters;
using *******.Models;

namespace *******.Fragments {
    public class Dashboard : GridLayoutBase {
        private ISharedPreferences pref;
        private SessionManager session;
        private string cookie;
        private DeviceModel deviceModel;
        private RecyclerView recyclerView;
        private RecyclerView.Adapter adapter;
//      private RecyclerView.LayoutManager layoutManager;
        private GridLayoutManager gridLayoutManager;
        private List<ItemData> itemData;
        private Bitmap lastPhotoBitmap;
        private Drawable lastPhotoDrawable;
        private static Activity activity;
        private ProgressDialog progressDialog;
        private TextView noData;
        private const string URL_DASHBOARD = "http://192.168.1.101/appapi/getdashboard";
        private const string URL_DATA = "http://192.168.1.101/appapi/getdata";

        public override void OnCreate(Bundle bundle) {
            base.OnCreate(bundle);

            activity = Activity;
            session = new SessionManager();
            pref = activity.GetSharedPreferences("UserSession", FileCreationMode.Private);
            cookie = pref.GetString("PHPSESSID", string.Empty);
        }

        public async override void OnStart() {
            base.OnStart();

            progressDialog = ProgressDialog.Show(activity, String.Empty, GetString(Resource.String.loading_text));
            progressDialog.Window.ClearFlags(WindowManagerFlags.DimBehind);

            await GetDevicesInfo();

            if (deviceModel.Error == "true" && deviceModel.ErrorType == "noSensors") {
                recyclerView.Visibility = ViewStates.Gone;
                noData.Visibility = ViewStates.Visible;

                progressDialog.Hide();

                return;
            } else {
                recyclerView.Visibility = ViewStates.Visible;
                noData.Visibility = ViewStates.Gone;

                await PopulateSensorStates();
            }

//          DisplayLastPhoto();

            adapter = new ViewAdapter(itemData);

            new System.Threading.Thread(new System.Threading.ThreadStart(() => {
                activity.RunOnUiThread(() => {
                    recyclerView.SetAdapter(adapter);
                });
            })).Start();

            progressDialog.Hide();
        }

        public async Task GetDevicesInfo() {
            var jsonFetcher = new JsonFetcher();
            JsonValue jsonDashboard = await jsonFetcher.FetchDataWithCookieAsync(URL_DASHBOARD, cookie);
            deviceModel = new DeviceModel();
            deviceModel = JsonConvert.DeserializeObject<DeviceModel>(jsonDashboard);
        }

        // Shows sensor states
        public async Task PopulateSensorStates() {
            itemData = new List<ItemData>();
            string lastValue = String.Empty;

            foreach (var sensor in this.deviceModel.Sensors) {
                var sensorImage = ResourcesCompat.GetDrawable(Resources, Resource.Drawable.smoke_red, null);

                switch (sensor.Type) {
                case "2":
                    var jsonFetcher = new JsonFetcher();
                    JsonValue jsonData = await jsonFetcher.FetchSensorDataAsync(URL_DATA, sensor.Id, "DESC", "1", cookie);
                    var deviceModel = new DeviceModel();
                    deviceModel = JsonConvert.DeserializeObject<DeviceModel>(jsonData);
                    lastValue = deviceModel.SensorData.Last().Value;
                    break;
                case "4":
                    await RenderLastCameraPhoto();
                    sensorImage = new BitmapDrawable(Resources, lastPhotoBitmap);
                    break;
                }

                itemData.Add(new ItemData() {
                    id = sensor.Id,
                    value = lastValue,
                    type = sensor.Type,
                    image = sensorImage,
                    title = sensor.Name.First().ToString().ToUpper() + sensor.Name.Substring(1).ToLower(),
                });
            }
        }

        // Shows the last camera photo
        public async Task RenderLastCameraPhoto() {
            if (deviceModel.Error == "true" && deviceModel.ErrorType == "noPhoto") {
                //TODO: Show a "No photo" picture
            } else {
                string url = deviceModel.LastPhotoLink;
                lastPhotoBitmap = await new ImageDownloader().GetImageBitmapFromUrlAsync(url, activity, 300, 300);
            }
        }

        public async void UpdateData(bool isSwipeRefresh) {
            await GetDevicesInfo();

            if (deviceModel.Error == "true" && deviceModel.ErrorType == "noSensors") {
                recyclerView.Visibility = ViewStates.Gone;
                noData.Visibility = ViewStates.Visible;

                return;
                } else {
                recyclerView.Visibility = ViewStates.Visible;
                noData.Visibility = ViewStates.Gone;

                await PopulateSensorStates();
            }

            adapter = new ViewAdapter(itemData);

            new System.Threading.Thread(new System.Threading.ThreadStart(() => {
                activity.RunOnUiThread(() => {
                    recyclerView.SetAdapter(adapter);
                });
            })).Start();

            adapter.NotifyDataSetChanged();
        }

        public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.Inflate(Resource.Layout.Dashboard, container, false);
            noData = view.FindViewById<TextView>(Resource.Id.no_data_title);

            SwipeRefreshLayout swipeRefreshLayout = view.FindViewById<SwipeRefreshLayout>(Resource.Id.swipe_container);
            //          swipeRefreshLayout.SetColorSchemeResources(Color.LightBlue, Color.LightGreen, Color.Orange, Color.Red);

            // On refresh button press/swipe, updates the recycler view with new data
            swipeRefreshLayout.Refresh += (sender, e) => {
                UpdateData(true);

                swipeRefreshLayout.Refreshing = false;
            };

            var gridLayoutManager = new GridLayoutManager(activity, 2);

            recyclerView = view.FindViewById<RecyclerView>(Resource.Id.dashboard_recycler_view);
            recyclerView.HasFixedSize = true;
            recyclerView.SetLayoutManager(gridLayoutManager);
            recyclerView.SetItemAnimator(new DefaultItemAnimator());
            recyclerView.AddItemDecoration(new SpaceItemDecoration(15));

            return view;
        }

        public class ViewAdapter : RecyclerView.Adapter {
            private List<ItemData> itemData;
            public string sensorId;
            public string sensorType;
            private ImageView imageId;
            private TextView sensorValue;
            private TextView sensorTitle;

            public ViewAdapter(List<ItemData> itemData) {
                this.itemData = itemData;
            }

            public class ItemView : RecyclerView.ViewHolder {
                public View mainView { get; set; }

                public string id { get; set; }

                public string type { get; set; }

                public ImageView image { get; set; }

                //              public TextView value { get; set; }

                public TextView title { get; set; }

                public ItemView(View view) : base(view) {
                    mainView = view;
                }
            }

            public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) {
                View itemLayoutView = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.DashboardItems, null);
                CardView cardView = itemLayoutView.FindViewById<CardView>(Resource.Id.dashboard_card_view);
                imageId = itemLayoutView.FindViewById<ImageView>(Resource.Id.sensor_image);
//              sensorValue = itemLayoutView.FindViewById<TextView>(Resource.Id.sensor_value);
                sensorTitle = itemLayoutView.FindViewById<TextView>(Resource.Id.sensor_title);

                var viewHolder = new ItemView(itemLayoutView) {
                    id = sensorId,
                    type = sensorType,
                    image = imageId,
//                  value = sensorValue,
                    title = sensorTitle
                };

                return viewHolder;
            }

            public override void OnBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
                ItemView itemHolder = viewHolder as ItemView;

                itemHolder.image.SetImageDrawable(itemData[position].image);

                if (itemData[position].type == "2") { // Temperature
                    itemHolder.title.Text = itemData[position].title + ": " + itemData[position].value;
                } else {
                    itemHolder.title.Text = itemData[position].title;
                }

                var bundle = new Bundle();
                var dualColumnList = new DualColumnList();
                var gallery = new Gallery();

                EventHandler clickUpdateViewEvent = ((sender, e) => {
                    bundle.PutString("id", itemData[position].id);
                    gallery.Arguments = bundle;
                    dualColumnList.Arguments = bundle;

                    if (itemData[position].type == "4") { // Camera
                        ((FragmentActivity)activity).ShowFragment(gallery, itemData[position].title, itemData[position].type, true);
                    } else {
                        ((FragmentActivity)activity).ShowFragment(dualColumnList, itemData[position].title, itemData[position].type, true);
                    }
                });

                itemHolder.image.Click += clickUpdateViewEvent;
//              itemHolder.value.Click += clickUpdateViewEvent;
                itemHolder.title.Click += clickUpdateViewEvent;
            }

            public override int ItemCount {
                get { return itemData.Count; }
            }
        }

        public class ItemData {
            public string id { get; set; }

            public string type { get; set; }

            public Drawable image { get; set; }

            public string value { get; set; }

            public string title { get; set; }
        }
    }
}

Fragment Layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipe_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:weightSum="1">
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="0.9"
            android:scrollbars="vertical">
            <android.support.v7.widget.RecyclerView
                android:id="@+id/dashboard_recycler_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
            <TextView
                android:text="@string/no_data_text"
                android:id="@+id/no_data_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="30sp"
                android:gravity="center"
                android:layout_centerInParent="true" />
        </RelativeLayout>
    </LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>

Fragment Items Layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/dashboard_card_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        android:foreground="?android:attr/selectableItemBackground">
        <ImageView
            android:id="@+id/sensor_image"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:paddingTop="5dp"
            android:layout_alignParentTop="true" />
    <!--        <TextView
            android:id="@+id/sensor_value"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="30sp"
            android:layout_below="@id/sensor_image"
            android:gravity="center" />-->
        <TextView
            android:id="@+id/sensor_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="23sp"
            android:layout_below="@id/sensor_image"
            android:gravity="center"
            android:layout_alignParentBottom="true" />
    </LinearLayout>
</android.support.v7.widget.CardView>
Muhammad answered 6/11, 2015 at 21:7 Comment(2)
Hey did you get solution?? am stuck alsoHavelock
For me, when I give actual width to recyclerView, its items autofit.Granite
G
157

You can calculate available number of columns, given a desired column width, and load the image as calculated. Define a static funtion to calculate as:

public class Utility {
    public static int calculateNoOfColumns(Context context, float columnWidthDp) { // For example columnWidthdp=180
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        float screenWidthDp = displayMetrics.widthPixels / displayMetrics.density;
        int noOfColumns = (int) (screenWidthDp / columnWidthDp + 0.5); // +0.5 for correct rounding to int.
        return noOfColumns;
    }
}

And then when using it in the activity or fragment you can do like this :

int mNoOfColumns = Utility.calculateNoOfColumns(getApplicationContext());

............
mGridLayoutManager = new GridLayoutManager(this, mNoOfColumns);
Gulch answered 20/7, 2016 at 4:32 Comment(10)
Awesome Code. Works Perfectly for me. when I replace 180 with 140.Kirkwall
What is 180 in the answer?Nola
180 is the width of your grid item. You can change it as per your convention.Bivouac
what if the item width changes according to its content? For example, first row contains two items, next row contains 4, next row contains 3 etc.Alterant
@AhmadJamilAlRasyid what's the problem you found ? could you tell us ?Gulch
@Gulch the calculation is correct but i have to figure out the width of grid by myself where my width of grid is dynamic, also if i add margin on each item it become worstHagood
Pay attention to + 0.5, rounding up could lead you to return a number of columns, that have no physical space with their margins. I think that is safer to have a minor number of columns.Ragman
Can we do the same thing without define the fix width?Ern
Works also in Kotlin when you change Java to Kotlin. What to do if this recommends 0 columns? Therefore I put a minimum of 1 column.Varion
Outstanding! It still helps in 2023. I tried other examples on the same orientation issue, but none worked properly. This one works just perfectly, and amazingly the same example here worked for me in Java. Thanks for this snippet. The only issue is, when the device orientation is changed, it reloads the recyclerview, thereby cancelling any edit done on one or more items in the recylerview. Is there a way to stop the reloading during orientation change?Chamber
R
31

The GridLayoutManager's constructor has an argument spanCount that is

The number of columns in the grid

You can initialize the manager with an integer resource value and provide different values for different screens (i.e. values-w600, values-large, values-land).

Rescue answered 9/11, 2015 at 10:39 Comment(1)
This is the cleanest way to do this, but maybe if you want to fit in tablets and other screens you will end with lots of files...Perambulate
I
15

I tried @Riten answer and worked funtastic!! But I wasn't happy with the hardcoded "180" So I modified to this:

    public class ColumnQty {
    private int width, height, remaining;
    private DisplayMetrics displayMetrics;

    public ColumnQty(Context context, int viewId) {

        View view = View.inflate(context, viewId, null);
        view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
        width = view.getMeasuredWidth();
        height = view.getMeasuredHeight();
        displayMetrics = context.getResources().getDisplayMetrics();
    }
    public int calculateNoOfColumns() {

        int numberOfColumns = displayMetrics.widthPixels / width;
        remaining = displayMetrics.widthPixels - (numberOfColumns * width);
//        System.out.println("\nRemaining\t" + remaining + "\nNumber Of Columns\t" + numberOfColumns);
        if (remaining / (2 * numberOfColumns) < 15) {
            numberOfColumns--;
            remaining = displayMetrics.widthPixels - (numberOfColumns * width);
        }
        return numberOfColumns;
    }
    public int calculateSpacing() {

        int numberOfColumns = calculateNoOfColumns();
//        System.out.println("\nNumber Of Columns\t"+ numberOfColumns+"\nRemaining Space\t"+remaining+"\nSpacing\t"+remaining/(2*numberOfColumns)+"\nWidth\t"+width+"\nHeight\t"+height+"\nDisplay DPI\t"+displayMetrics.densityDpi+"\nDisplay Metrics Width\t"+displayMetrics.widthPixels);
        return remaining / (2 * numberOfColumns);
    }
}

Where "viewId" is the layout to be used as views in the RecyclerView like in R.layout.item_for_recycler

Not sure though about the impact of View.inflate as I only use it to get the Width, nothing else.

Then on the GridLayoutManager I do:

GridLayoutManager gridLayoutManager = new GridLayoutManager(this, Utility.columnQty(this, R.layout.item_for_recycler));

UPDATE: I added more lines to the code as I use it to get a minimum width spacing in the Grid. Calculate spacing:

recyclerPatternsView.addItemDecoration(new GridSpacing(columnQty.calculateSpacing()));

GridSpacing:

public class GridSpacing extends RecyclerView.ItemDecoration {
    private final int spacing;

    public GridSpacing(int spacing) {
        this.spacing = spacing;
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        outRect.left = spacing;
        outRect.right = spacing;
        outRect.bottom = spacing;
        outRect.top = spacing;
    }
}
Impanation answered 13/6, 2017 at 20:35 Comment(5)
Hi Racu, I tried you method, but view.getMeasuredWidth() always returns 0. Any idea?Cyrenaica
No idea, I can only think that maybe the xml, you might have "0dp". I'll update the whole class, as I made some changes.Impanation
@Cyrenaica I updated to my latest version, note that I use it as well to set the distance between items. The "15" there, guarantees me that the space betwenn items will be at least 15px, if it is less, the method will remove one column and recalculate the spacing. Wish I could make the "15px" "15dp" or something less hardcoded. GLImpanation
for me this didnt want to work until i added a dp to pixel conversion in betweenToscano
private float convertDpToPixel(float dp, Context context) { return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT); }Toscano
P
13

A simple Kotlin extension function. Pass the width in DP (same as the overall width in the item xml)

/**
 * @param columnWidth - in dp
 */
fun RecyclerView.autoFitColumns(columnWidth: Int) {
    val displayMetrics = this.context.resources.displayMetrics
    val noOfColumns = ((displayMetrics.widthPixels / displayMetrics.density) / columnWidth).toInt()
    this.layoutManager = GridLayoutManager(this.context, noOfColumns)
}

Call it like every other extension...

myRecyclerView.autoFitColumns(200)
Pie answered 19/8, 2020 at 20:17 Comment(1)
worked like champ!Toomey
G
4
public class AutoFitGridLayoutManager extends GridLayoutManager {

private int columnWidth;
private boolean columnWidthChanged = true;

public AutoFitGridLayoutManager(Context context, int columnWidth) {
    super(context, 1);

    setColumnWidth(columnWidth);
}


public void setColumnWidth(int newColumnWidth) {
    if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
        columnWidth = newColumnWidth;
        columnWidthChanged = true;
    }
}

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (columnWidthChanged && columnWidth > 0) {
        int totalSpace;
        if (getOrientation() == VERTICAL) {
            totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
        } else {
            totalSpace = getHeight() - getPaddingTop() - getPaddingBottom();
        }
        int spanCount = Math.max(1, totalSpace / columnWidth);
        setSpanCount(spanCount);
        columnWidthChanged = false;
    }
    super.onLayoutChildren(recycler, state);
}

}

now you can set LayoutManager to recycle view

here i have set 250px

 AutoFitGridLayoutManager layoutManager = new AutoFitGridLayoutManager(this, 250);
 recycleView.setLayoutManager(layoutManager)

show the belove image

Gunny answered 3/4, 2019 at 4:59 Comment(1)
Could you, please, attach an image?Kneehole
S
0

Constructor new GridLayoutManager(activity, 2) is about GridLayoutManager(Context context, int spanCount) where spanCount is the number of columns in the grid.

Best way is to check window/view width and base on this width count how many spans you want to show.

Silverweed answered 6/11, 2015 at 21:21 Comment(0)
E
-1

Use this function, and put margins for cell layout in XML instead using decoration.

public int getNumberOfColumns() {
        View view = View.inflate(this, R.layout.row_layout, null);
        view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
        int width = view.getMeasuredWidth();
        int count = getResources().getDisplayMetrics().widthPixels / width;
        int remaining = getResources().getDisplayMetrics().widthPixels - width * count;
        if (remaining > width - 15)
            count++;
        return count;
    }
Expanse answered 22/11, 2018 at 3:35 Comment(1)
width - 15 ? man .. this cannot be good.Teryn
H
-3

Set in recyclerView initialization:

recyclerView.setLayoutManager(new GridLayoutManager(this, 4));
Homopterous answered 1/8, 2020 at 5:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.