45

Like this previous person, I have unwanted overlap between GridView items:

GridView items overlapping

Notice the text, in every column except the rightmost one.

Where I differ from that previous question is that I don't want a constant row height. I want the row height to vary to accommodate the tallest content in each row, for efficient use of screen space.

Looking at the source for GridView (not the authoritative copy, but kernel.org is still down), we can see in fillDown() and makeRow() that the last View seen is the "reference view": the row's height is set from the height of that View, not from the tallest one. This explains why the rightmost column is ok. Unfortunately, GridView is not well set-up for me to fix this by inheritance. All the relevant fields and methods are private.

So, before I take the well-worn bloaty path of "clone and own", is there a trick I'm missing here? I could use a TableLayout, but that would require me to implement numColumns="auto_fit" myself (since I want e.g. just one long column on a phone screen), and it also wouldn't be an AdapterView, which this feels like it ought to be.

Edit: in fact, clone and own is not practical here. GridView depends on inaccessible parts of its parent and sibling classes, and would result in importing at least 6000 lines of code (AbsListView, AdapterView, etc.)

Community
  • 1
  • 1
Chris Boyle
  • 11,168
  • 7
  • 46
  • 62
  • 1
    For fixing Height, this link can be a solution: http://stackoverflow.com/questions/11411472/android-gridviews-row-height-max-of-item/16018067#16018067 – kinghomer Apr 15 '13 at 14:43
  • 1
    It better to use recycle view. It will handle this kind of thing correctly. – Ishan Fernando Jul 06 '17 at 18:11

6 Answers6

27

I used a static array to drive max heights for the row. This is not perfect since the earlier columns will not be resized until the cell is redisplayed. Here is the code for the inflated reusable content view.

Edit: I got this work correctly but I had pre-measure all cells before rendering. I did this by subclassing GridView and adding a measuring hook in the onLayout method.

/**
 * Custom view group that shares a common max height
 * @author Chase Colburn
 */
public class GridViewItemLayout extends LinearLayout {

    // Array of max cell heights for each row
    private static int[] mMaxRowHeight;

    // The number of columns in the grid view
    private static int mNumColumns;

    // The position of the view cell
    private int mPosition;

    // Public constructor
    public GridViewItemLayout(Context context) {
        super(context);
    }

    // Public constructor
    public GridViewItemLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * Set the position of the view cell
     * @param position
     */
    public void setPosition(int position) {
        mPosition = position;
    }

    /**
     * Set the number of columns and item count in order to accurately store the
     * max height for each row. This must be called whenever there is a change to the layout
     * or content data.
     * 
     * @param numColumns
     * @param itemCount
     */
    public static void initItemLayout(int numColumns, int itemCount) {
        mNumColumns = numColumns;
        mMaxRowHeight = new int[itemCount];
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // Do not calculate max height if column count is only one
        if(mNumColumns <= 1 || mMaxRowHeight == null) {
            return;
        }

        // Get the current view cell index for the grid row
        int rowIndex = mPosition / mNumColumns;
        // Get the measured height for this layout
        int measuredHeight = getMeasuredHeight();
        // If the current height is larger than previous measurements, update the array
        if(measuredHeight > mMaxRowHeight[rowIndex]) {
            mMaxRowHeight[rowIndex] = measuredHeight;
        }
        // Update the dimensions of the layout to reflect the max height
        setMeasuredDimension(getMeasuredWidth(), mMaxRowHeight[rowIndex]);
    }
}

Here is the measuring function in my BaseAdapter subclass. Note that I have a method updateItemDisplay that sets all appropriate text and images on the view cell.

    /**
     * Run a pass through each item and force a measure to determine the max height for each row
     */
    public void measureItems(int columnWidth) {
        // Obtain system inflater
        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        // Inflate temp layout object for measuring
        GridViewItemLayout itemView = (GridViewItemLayout)inflater.inflate(R.layout.list_confirm_item, null);

        // Create measuring specs
        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

        // Loop through each data object
        for(int index = 0; index < mItems.size(); index++) {
            String[] item = mItems.get(index);

            // Set position and data
            itemView.setPosition(index);
            itemView.updateItemDisplay(item, mLanguage);

            // Force measuring
            itemView.requestLayout();
            itemView.measure(widthMeasureSpec, heightMeasureSpec);
        }
    }

And finally, here is the GridView subclass set up to measure view cells during layout:

/**
 * Custom subclass of grid view to measure all view cells
 * in order to determine the max height of the row
 * 
 * @author Chase Colburn
 */
public class AutoMeasureGridView extends GridView {

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

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

    public AutoMeasureGridView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if(changed) {
            CustomAdapter adapter = (CustomAdapter)getAdapter();

            int numColumns = getContext().getResources().getInteger(R.integer.list_num_columns);
            GridViewItemLayout.initItemLayout(numColumns, adapter.getCount());

            if(numColumns > 1) {
                int columnWidth = getMeasuredWidth() / numColumns;
                adapter.measureItems(columnWidth);
            }
        }
        super.onLayout(changed, l, t, r, b);
    }
}

The reason I have the number of columns as a resource is so that I can have a different number based on orientation, etc.

Chase
  • 11,081
  • 7
  • 39
  • 39
  • Can you include your GridView subclass code here, or the relevant parts of it? – Chris Boyle Sep 30 '11 at 23:36
  • Here you go! I hope it helps. – Chase Oct 03 '11 at 03:13
  • Thanks! I will switch over to this at some point, it should look quite a bit better than the TableLayout. :-) – Chris Boyle Oct 03 '11 at 10:07
  • @ChrisBoyle kindly request to send any demo link for that – Parag Chauhan Apr 17 '12 at 11:27
  • 4
    I like your idea, but before testing or using I have many doubts. First, you are storing all row heights in static variable. I think it will mess up when using more than one GridView inside same View. Second, what if 'numColumns' is 'auto_fit'. Third, when measuring 'columnWidth' inside 'onLayout' of 'AutoMeasureGridView'. You are using 'getMeasuredWidth() / numColumns'. But what about horizontal spacing ? Shouldn't it be 'getMeasuredWidth() / (numColumns + ((numColumns-1) * horizontalSpacing))' ? Thanks :) – xmen Mar 23 '13 at 03:00
  • I can't seem to figure out how you're doing this, could you provide me with an example? – Gee.E Dec 04 '13 at 10:33
  • @xmenW.K. To retrieve numColumns if it's "auto_fit", check this stackoverflow.com/a/15572133/1681789 – Rodrigo Venancio Mar 13 '14 at 18:16
  • 2
    Please can you upload your itemView.updateItemDisplay(item, mLanguage); method, it is confusing me, this answer is helpful for me, but i am confused about updateItemDisplay() method – habib ullah Jun 06 '14 at 14:27
  • Question: I am using this GridView implementation, everytime I close the app via "home" or lock the screen when I get back to it it messes with the row height. Specifically it groups the items to measure the height in pairs instead of doing it by rows. Maybe I can add some code to the restored state of the activity? – Angmar Mar 04 '15 at 11:41
1

Based on the info from Chris, I used this workaround making use of the reference-View used by the native GridView when determining the height of other GridView items.

I created this GridViewItemContainer custom class:

/**
 * This class makes sure that all items in a GridView row are of the same height.
 * (Could extend FrameLayout, LinearLayout etc as well, RelativeLayout was just my choice here)
 * @author Anton Spaans
 *
*/
public class GridViewItemContainer extends RelativeLayout {
private View[] viewsInRow;

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

public GridViewItemContainer(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}

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

public void setViewsInRow(View[] viewsInRow) {
    if  (viewsInRow != null) {
        if (this.viewsInRow == null) {
            this.viewsInRow = Arrays.copyOf(viewsInRow, viewsInRow.length);
        }
        else {
            System.arraycopy(viewsInRow, 0, this.viewsInRow, 0, viewsInRow.length);
        }
    }
    else if (this.viewsInRow != null){
        Arrays.fill(this.viewsInRow, null);
    }
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}

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

    if (viewsInRow == null) {
        return;
    }

    int measuredHeight = getMeasuredHeight();
    int maxHeight      = measuredHeight;
    for (View siblingInRow : viewsInRow) {
        if  (siblingInRow != null) {
            maxHeight = Math.max(maxHeight, siblingInRow.getMeasuredHeight());
        }
    }

    if (maxHeight == measuredHeight) {
        return;
    }

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    switch(heightMode) {
    case MeasureSpec.AT_MOST:
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.min(maxHeight, heightSize), MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        break;

    case MeasureSpec.EXACTLY:
        // No debate here. Final measuring already took place. That's it.
        break;

    case MeasureSpec.UNSPECIFIED:
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        break;

    }
}

In your adapter's getView method, either wrap your convertView as a child in a new GridViewItemContainer or make this one the top XML element of your item layout:

        // convertView has been just been inflated or came from getView parameter.
        if (!(convertView instanceof GridViewItemContainer)) {
            ViewGroup container = new GridViewItemContainer(inflater.getContext());

            // If you have tags, move them to the new top element. E.g.:
            container.setTag(convertView.getTag());
            convertView.setTag(null);

            container.addView(convertView);
            convertView = container;
        }
        ...
        ...
        viewsInRow[position % numColumns] = convertView;
        GridViewItemContainer referenceView = (GridViewItemContainer)convertView;
        if ((position % numColumns == (numColumns-1)) || (position == getCount()-1)) {
            referenceView.setViewsInRow(viewsInRow);
        }
        else {
            referenceView.setViewsInRow(null);
        }

Where numColumns is the number of columns in the GridView and 'viewsInRow' is an list of View on the current row of where 'position' is located.

Streets Of Boston
  • 12,186
  • 2
  • 23
  • 27
  • Works well, until two *consequent* rows expose the same issue: then they overlap again on the fist one that was initially too tall. Fixing the height of what is subject to change in height, on the other side, always works. See http://stackoverflow.com/a/2379461/1865860 :) – dentex Apr 15 '14 at 13:13
1

I did so many research but found incomplete answer or had tough with understanding what going on with solution but finally found an answer that fit perfectly with proper explanation.

My problem was to fit gridview item into height properly. This Grid-view worked great when all of your views are the same height. But when your views have different heights, the grid doesn't behave as expected. Views will overlap each other, causing an an-aesthetically pleasing grid.

Here Solution I used this class in XML layout.

I used this solution, and this is working very well, thanks a lot.--Abhishek Mittal

opalfire
  • 91
  • 1
  • 9
0

If you convert your GridView or ListView to a RecyclerView, this issue will not happen. And you won't need to make a custom GridView class.

Mr-IDE
  • 4,530
  • 1
  • 36
  • 44
0

This is not the correct solution which I am mentioned below, but can be workaround depends on your requirement.

Just set the height of view fix(in some dp i.e.- 50dp) from your child layout of gridview, so that it can be Wrapped.

        <TextView
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:ellipsize="end"
            android:textColor="@color/text_color"
            android:textSize="13dp"
            android:textStyle="normal" />
-2

Giving weight to your GridView also works on GridViews inside LinearLayouts as a child. This way GridView fills the viewport with its children so you are able to view it's items as long as they fit the screen (then you scroll).

But always avoid using GridViews inside ScrollViews. Otherwise you will need to calculate each child's height and reassign them as Chase answered above.

<GridView
    android:id="@+id/gvFriends"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:verticalSpacing="5dp"
    android:horizontalSpacing="5dp"
    android:clipChildren="false"
    android:listSelector="@android:color/transparent"
    android:scrollbarAlwaysDrawHorizontalTrack="false"
    android:scrollbarAlwaysDrawVerticalTrack="false"
    android:stretchMode="columnWidth"
    android:scrollbars="none"
    android:numColumns="4"/>
Merve Gencer
  • 87
  • 1
  • 5
  • I have a gridView with items of varying height. So far the only fix I found is setting a fixed height on the items. I'm pretty sure your solution is incorrect. First of all, layout_weight is not inherited by child elements (it would be really weird if it was), and even if it was, it still doesn't solve the problem. As described in the question, the problem is that GridView takes the last element of the row as the reference view, instead of the tallest one. If would be great if your solution does work, but please provide a more thorough example in this case to show that it works. – wvdz Mar 15 '15 at 14:37
  • The problem is having a gridview inside a scrollview and my solution fills the linear layout (the only child of the scrollview) which makes all its children visible. if you dont have a scrollview then you dont need to worry about row heights because the gridview sets the row height according to its tallest child if you make your adapter's view to wrap_content. That's what i implemented and achieved. – Merve Gencer Mar 16 '15 at 14:55