How to handle GridView with cell of different heights?
Asked Answered
W

3

7

NOTE: OK, I admit the title is a bit vague, but English is not my main language, and I'm not sure how to describe the problem in one sentance.

Background

I'm trying to create an app that shows all apps information on a gridView.

The gridView has its numColumns set to auto_fit , so that its number of columns will be set nicely for all types of screens.

The problem

Since each app's information can sometimes be long, the cell height can be different from those that are next to it.

I want the grid cells to be of the same width (which works fine on all cases), and for each row, the height should be determined by the max height of the row's cells.

This causes weird things to occur:

  1. Rare: some cells are becoming empty . sometimes, when scrolling and returning there, the cells get filled...
  2. Quite common: some cells draw over other cells.
  3. Quite common: some cells don't take the space they can take , leaving empty space that cannot be clicked.
  4. Very rare: when playing with the scrolling, if you scroll all the way to the top, you get the entire grid to be empty and not scrollable ...

Here is a screenshot showing both #2 and #3 issues (they don't usually show together) :

enter image description here

Code I've used

This is the xml of the grid cell :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/list_divider_holo_dark" >

    <ImageView
        android:id="@+id/appIconImageView"
        android:layout_width="48dip"
        android:layout_height="48dip"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:adjustViewBounds="true"
        android:contentDescription="@string/app_icon"
        android:src="@drawable/ic_menu_help" />

    <TextView
        android:id="@+id/appLabelTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_toLeftOf="@+id/isSystemAppImageView"
        android:layout_toRightOf="@+id/appIconImageView"
        android:ellipsize="marquee"
        android:text="label"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textStyle="bold"
        tools:ignore="HardcodedText" />

    <TextView
        android:id="@+id/appDescriptionTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/appLabelTextView"
        android:layout_below="@+id/appLabelTextView"
        android:layout_toLeftOf="@+id/isSystemAppImageView"
        android:ellipsize="marquee"
        android:text="description"
        tools:ignore="HardcodedText" />

    <ImageView
        android:id="@+id/isSystemAppImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/appDescriptionTextView"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:adjustViewBounds="true"
        android:contentDescription="@string/content_description_this_is_a_system_app"
        android:src="@drawable/stat_sys_warning" />

</RelativeLayout>

What I've tried

I've tried multiple possible solutions:

  1. using HorizontalListView seems to solve this, but then it doesn't allow clicking/long clicking on items.

  2. I've found a nice library for making the textViews have a "marquee" effect (here) , but its license isn't so permissive.

  3. Setting the cell height as fixed one or setting the textViews to have fixed lines count also work, but then it cuts down (part of) the content of the textViews without the ability to show them.

  4. I've also tried using linearLayouts instead of RelativeLayout, but it didn't help.

  5. a possible solution is to get the max height used by all views on the gridView (using getView on all items) , similar to this code, but it assumes you know the number of columns , and that you do want all cells to be of the same height.

The question

How should I handle this?

Should I really use something like StaggeredGrid ?

Is there a way to put the textViews to stay fixed in size yet allow them to show their content using scrolling (manually or automatically) ?

Wileen answered 18/1, 2014 at 12:14 Comment(6)
Have you tried setting layout_height="wrap_content" of the enclosing relative layout?Befool
@KubaSpatny yes. sadly, it didn't help. :(Wileen
Well StaggeredGrid would definitely work, but it might be an overkill. You meantioned using marquee effect library, have you tried implementing it yourself?Befool
i didn't, since i'm not sure how i should do it and how long it will take to do it. i was thinking that maybe it should extend from horizontalScrollView, and yet it won't be touchable. i also need to make it customized enough to tell it how to move.Wileen
There's android:maxLines="4" tag (or android:maxLength="10") for TextView and android:ellipsize="marquee" however the problem in listview is that it's going to scroll only if the certain view is in focus. So you would have to make a class extending TextView as seen here #1828251Befool
yes i know. it's a part of other tests i've tried to use it. i've now tried the link you've sent, and used the marquee configurations i've used, and it doesn't scroll at all (also not auto-scrolling). seems the marquee feature only works when using singleLine="true" . what do you think can be done with the horizontalScrollView so that it will allow manual scrolling (by the user), yet don't interfere with touches on the gridView cells? i've tried this: https://mcmap.net/q/395192/-passing-touch-events-to-the-parent-view , but it didn't work... for some reason, the gridView identifies it and blocks touches on the cells...Wileen
W
0

OK, i've found a nice solution for this, which will make the rows depend on their children, but for this I had to make some compromises :

  1. it's an adapter for a listView , which has horizontal linear layouts for its rows.

  2. whoever uses this code should set the selector of the cells and choose what to do on clicking or long clicking.

  3. whoever uses this code should be ready to prepare an empty cell for the extra last cells of the listView.

  4. the listView that is used should have its listSelector set to transparent, to disallow clicking on rows.

I hope anyone could make a better solution that won't need extra views.

sample usage:

_listAdapter=new GriddedListViewAdapter();
_listAdapter.setNumberOfColumns(numberOfColumns);
-listAdapter.setItems(items);
_listView.setAdapter(_listAdapter);

Here's the code:

public abstract class GriddedListViewAdapter<ItemType> extends BaseAdapter
  {
  private int                       _numColumns =1;
  private final List<Row<ItemType>> _rows       =new ArrayList<Row<ItemType>>();
  protected final Context           _context    =App.global();
  private List<ItemType>            _items;

  public void setItems(final List<ItemType> items)
    {
    _items=items;
    prepareRows();
    notifyDataSetChanged();
    }

  private void prepareRows()
    {
    _rows.clear();
    final int itemsCount=_items.size();
    for(int i=0;i<itemsCount;i+=_numColumns)
      {
      final Row<ItemType> row=new Row<ItemType>();
      row.startIndex=i;
      row.items=new ArrayList<ItemType>(_numColumns);
      for(int j=0;j<_numColumns;++j)
        {
        ItemType item;
        if(i+j<itemsCount)
          item=_items.get(i+j);
        else item=null;
        row.items.add(item);
        }
      _rows.add(row);
      }
    }

  public void setNumberOfColumns(final int numColumns)
    {
    if(_numColumns==numColumns)
      return;
    _numColumns=numColumns;
    if(_items!=null)
      {
      prepareRows();
      notifyDataSetChanged();
      }
    }

  @Override
  public final int getCount()
    {
    return _rows.size();
    }

  @Override
  public final Row<ItemType> getItem(final int position)
    {
    return _rows.get(position);
    }

  @Override
  public final long getItemId(final int position)
    {
    return position;
    }

  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  @Override
  public final View getView(final int position,final View convertView,final ViewGroup parent)
    {
    final Row<ItemType> row=getItem(position);
    IcsLinearLayout rowLayout=(IcsLinearLayout)convertView;
    if(rowLayout==null)
      {
      rowLayout=new IcsLinearLayout(_context,null);
      rowLayout.setMeasureWithLargestChildEnabled(true);
      rowLayout.setShowDividers(IcsLinearLayout.SHOW_DIVIDER_MIDDLE);
      rowLayout.setDividerDrawable(_context.getResources().getDrawable(R.drawable.list_divider_holo_dark));
      rowLayout.setOrientation(LinearLayout.HORIZONTAL);
      rowLayout.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT));
      }
    final int childCount=rowLayout.getChildCount();
    for(int i=childCount;i>_numColumns;--i)
      rowLayout.removeViewAt(i-1);
    // reuse previous views of the row if possible
    for(int i=0;i<_numColumns;++i)
      {
      // reuse old views if possible
      final View cellConvertView=i<childCount ? rowLayout.getChildAt(i) : null;
      // fill cell with data
      final View cellView=getCellView(row.items.get(i),row.startIndex+i,cellConvertView,rowLayout);
      LinearLayout.LayoutParams layoutParams=(LinearLayout.LayoutParams)cellView.getLayoutParams();
      if(layoutParams==null)
        {
        layoutParams=new LinearLayout.LayoutParams(0,LayoutParams.MATCH_PARENT,1);
        layoutParams.gravity=Gravity.CENTER_VERTICAL;
        cellView.setLayoutParams(layoutParams);
        }
      else
        {
        final boolean needSetting=layoutParams.weight!=1||layoutParams.width!=0||layoutParams.height!=LayoutParams.MATCH_PARENT;
        if(needSetting)
          {
          layoutParams.width=0;
          layoutParams.height=LayoutParams.MATCH_PARENT;
          layoutParams.gravity=Gravity.CENTER_VERTICAL;
          layoutParams.weight=1;
          cellView.setLayoutParams(layoutParams);
          }
        }
      if(cellConvertView==null)
        rowLayout.addView(cellView);
      }
    return rowLayout;
    }

  @Override
  public final int getViewTypeCount()
    {
    return super.getViewTypeCount();
    }

  @Override
  public final int getItemViewType(final int position)
    {
    return super.getItemViewType(position);
    }

  /**
   * should handle getting a single cell view. <br/>
   * NOTE:read the parameters description carefully !
   * 
   * @param item
   * the item that is associated with the cell. if null, you should prepare an empty cell
   * @param rawPosition the position within the original list of items that is associated with the cell
   * @param convertView
   * a recycled cell. you must use it when it's not null, fill it with data, and return it
   * @param parent
   * the parent of the view. you should use it for inflating the view (but don't attach the view to the
   * parent)
   */
  public abstract View getCellView(ItemType item,int rawPosition,View convertView,ViewGroup parent);

  // ////////////////////////////////////
  // Row//
  // /////
  private static class Row<ItemType>
    {
    int                 startIndex;
    ArrayList<ItemType> items;
    }
  }
Wileen answered 1/2, 2014 at 22:30 Comment(0)
W
6

An alternative to the above solution is to use RecyclerView and GridLayoutManager:

recyclerView.setHasFixedSize(false);
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
Wileen answered 20/10, 2015 at 14:47 Comment(0)
W
0

OK, i've found a nice solution for this, which will make the rows depend on their children, but for this I had to make some compromises :

  1. it's an adapter for a listView , which has horizontal linear layouts for its rows.

  2. whoever uses this code should set the selector of the cells and choose what to do on clicking or long clicking.

  3. whoever uses this code should be ready to prepare an empty cell for the extra last cells of the listView.

  4. the listView that is used should have its listSelector set to transparent, to disallow clicking on rows.

I hope anyone could make a better solution that won't need extra views.

sample usage:

_listAdapter=new GriddedListViewAdapter();
_listAdapter.setNumberOfColumns(numberOfColumns);
-listAdapter.setItems(items);
_listView.setAdapter(_listAdapter);

Here's the code:

public abstract class GriddedListViewAdapter<ItemType> extends BaseAdapter
  {
  private int                       _numColumns =1;
  private final List<Row<ItemType>> _rows       =new ArrayList<Row<ItemType>>();
  protected final Context           _context    =App.global();
  private List<ItemType>            _items;

  public void setItems(final List<ItemType> items)
    {
    _items=items;
    prepareRows();
    notifyDataSetChanged();
    }

  private void prepareRows()
    {
    _rows.clear();
    final int itemsCount=_items.size();
    for(int i=0;i<itemsCount;i+=_numColumns)
      {
      final Row<ItemType> row=new Row<ItemType>();
      row.startIndex=i;
      row.items=new ArrayList<ItemType>(_numColumns);
      for(int j=0;j<_numColumns;++j)
        {
        ItemType item;
        if(i+j<itemsCount)
          item=_items.get(i+j);
        else item=null;
        row.items.add(item);
        }
      _rows.add(row);
      }
    }

  public void setNumberOfColumns(final int numColumns)
    {
    if(_numColumns==numColumns)
      return;
    _numColumns=numColumns;
    if(_items!=null)
      {
      prepareRows();
      notifyDataSetChanged();
      }
    }

  @Override
  public final int getCount()
    {
    return _rows.size();
    }

  @Override
  public final Row<ItemType> getItem(final int position)
    {
    return _rows.get(position);
    }

  @Override
  public final long getItemId(final int position)
    {
    return position;
    }

  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  @Override
  public final View getView(final int position,final View convertView,final ViewGroup parent)
    {
    final Row<ItemType> row=getItem(position);
    IcsLinearLayout rowLayout=(IcsLinearLayout)convertView;
    if(rowLayout==null)
      {
      rowLayout=new IcsLinearLayout(_context,null);
      rowLayout.setMeasureWithLargestChildEnabled(true);
      rowLayout.setShowDividers(IcsLinearLayout.SHOW_DIVIDER_MIDDLE);
      rowLayout.setDividerDrawable(_context.getResources().getDrawable(R.drawable.list_divider_holo_dark));
      rowLayout.setOrientation(LinearLayout.HORIZONTAL);
      rowLayout.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT));
      }
    final int childCount=rowLayout.getChildCount();
    for(int i=childCount;i>_numColumns;--i)
      rowLayout.removeViewAt(i-1);
    // reuse previous views of the row if possible
    for(int i=0;i<_numColumns;++i)
      {
      // reuse old views if possible
      final View cellConvertView=i<childCount ? rowLayout.getChildAt(i) : null;
      // fill cell with data
      final View cellView=getCellView(row.items.get(i),row.startIndex+i,cellConvertView,rowLayout);
      LinearLayout.LayoutParams layoutParams=(LinearLayout.LayoutParams)cellView.getLayoutParams();
      if(layoutParams==null)
        {
        layoutParams=new LinearLayout.LayoutParams(0,LayoutParams.MATCH_PARENT,1);
        layoutParams.gravity=Gravity.CENTER_VERTICAL;
        cellView.setLayoutParams(layoutParams);
        }
      else
        {
        final boolean needSetting=layoutParams.weight!=1||layoutParams.width!=0||layoutParams.height!=LayoutParams.MATCH_PARENT;
        if(needSetting)
          {
          layoutParams.width=0;
          layoutParams.height=LayoutParams.MATCH_PARENT;
          layoutParams.gravity=Gravity.CENTER_VERTICAL;
          layoutParams.weight=1;
          cellView.setLayoutParams(layoutParams);
          }
        }
      if(cellConvertView==null)
        rowLayout.addView(cellView);
      }
    return rowLayout;
    }

  @Override
  public final int getViewTypeCount()
    {
    return super.getViewTypeCount();
    }

  @Override
  public final int getItemViewType(final int position)
    {
    return super.getItemViewType(position);
    }

  /**
   * should handle getting a single cell view. <br/>
   * NOTE:read the parameters description carefully !
   * 
   * @param item
   * the item that is associated with the cell. if null, you should prepare an empty cell
   * @param rawPosition the position within the original list of items that is associated with the cell
   * @param convertView
   * a recycled cell. you must use it when it's not null, fill it with data, and return it
   * @param parent
   * the parent of the view. you should use it for inflating the view (but don't attach the view to the
   * parent)
   */
  public abstract View getCellView(ItemType item,int rawPosition,View convertView,ViewGroup parent);

  // ////////////////////////////////////
  // Row//
  // /////
  private static class Row<ItemType>
    {
    int                 startIndex;
    ArrayList<ItemType> items;
    }
  }
Wileen answered 1/2, 2014 at 22:30 Comment(0)
B
0

Here is a working solution: http://obduro.nl/blog/the-solution-of-android-gridview-overlap/

The solution has explained on that link is to calculate each row height based on the height of all its elements (using maximum height).

It does that by catching the onScrollChanged event and updating heights everytime the FirstVisiblePosition changes.

After testing this, it works beautifully, except for one catch: Once a row size has been increased it will not decrease.

Actual code can be found here: https://github.com/JJdeGroot/AutoGridView

Balancer answered 17/10, 2015 at 19:22 Comment(4)
Nice, but there is also the solution of RecyclerViewWileen
I've always seen RecyclerView like a ListView, not a GridView, but I'm probably wrong? Can you have multiple columns in RecyclerView? The problem with GridView is that it has bugs.Balancer
The RecyclerView can show items however you wish. The only problem with it, is its complexity due to it high modularity, and that it misses some things we got used to have, like fast-scroller, header and footer. For fast-scroller, I've made a library (here: github.com/AndroidDeveloperLB/… ) . For footer and header, you need to set the first item and last item as being of a different type, sadly, similar to ListView.Wileen
Then, it might be useful to post an answer using the RecyclerView ?Balancer

© 2022 - 2024 — McMap. All rights reserved.