32

I am trying to display a dynamically-growing list of strings with a checkbox in a GridView, which is itself in a TableLayout.

I can display these "checkboxed" strings fine in a row. My problem occurs when I let the user dynamically add new strings in the GridView.

I created a custom adapter that receives the list of strings. Say we have n strings. The adapter returns 'n + 1' for the items count; in getView, it returns:

  • a View with a LinearLayout, itself having a CheckBox and an EditText for the first n items,
  • a LinearLayout with a simple button, with a '+' caption for the 'n + 1'th item.

So far so good. When the '+' button is clicked, I add an empty string to the list of strings and call notifyDataSetChanged in the adapter.

The GridView redraws itself with one more item. BUT it keeps its original height and creates a vertical scrollbar. I'd like the GridView to expand its height (i.e. take up more space on screen and show all items).

I've tried to change the screen to a vertical LinearLayout instead of a TableLayout, but the results is the same.

Here is the layout of my screen:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:scrollbars="none">
<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    >

    <!-- other lines omitted -->

    <LinearLayout android:layout_width="fill_parent" 
                  android:layout_height="wrap_content" >
        <TextView
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:text="@string/wine_rack_explanation"
            android:layout_gravity="center_vertical" />
          <GridView
            android:layout_width="fill_parent" 
            android:layout_height="wrap_content"
            android:id="@+id/gvRack"
            android:columnWidth="90dp"    
            android:numColumns="auto_fit" />
    </LinearLayout>

<!-- other lines omitted -->

</LinearLayout>
</ScrollView>

The checkbox + string item from the adapter is defined like this:

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"

  android:layout_width="wrap_content"
  android:layout_height="wrap_content">
    <CheckBox android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:id="@+id/cbCoordinate"
              android:checked="true"
              />
    <EditText android:id="@+id/txtCoordinate"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content" />
</LinearLayout>

The '+' button item is defined like this:

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content">

    <Button android:id="@+id/btnAddBottle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/add_bottle_caption" />
</LinearLayout>

I've tried to call invalidate or invalideViews on the GridView after an item is added. Also called invalidate on the TableLayout and TableRow (in the previous layout of the screen). No success.

Any idea why the GridView refuses to extend its height ?

(Note that I am completely open to using an other viewgroup than the GridView)

Timores
  • 14,439
  • 3
  • 46
  • 46

7 Answers7

60

Use this code

public class MyGridView  extends GridView {

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

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

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

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
}
Damien Locque
  • 1,810
  • 2
  • 20
  • 42
Raj008
  • 3,539
  • 2
  • 28
  • 26
  • 4
    This works well, but as i add many items, i am now finding a large empty space at the bottom of the GridView. I couldn't accord this space to any other reason, as i have checked with Developer Option's Show Layout Outlines Feature. Please help. – Sri Krishna Feb 27 '15 at 12:03
  • If your items have different heights, this solution may not work properly. For constant height items, this is perfect. – Ziad Akiki Dec 04 '16 at 16:48
  • 1
    Its work!!! Yea, Android has issue when GridView or ListView is inside ScrollView. But your solution makes my program worked fine. Thanks~~~ – Ellie Tam Jun 13 '17 at 11:21
  • @ZiadAkiki For items with different sizes this solution does not work as you described. How to make the same for items with different sizes ?? Is it possible or Android has some issue regarding this ? – Denis Kotov Feb 21 '18 at 23:09
  • @DenisKotov You can force all the items to have the height of the biggest one. You just calculate the height of each one and after adding the items, adjust the height of the others. – Ziad Akiki Feb 24 '18 at 09:43
10

I have done this way:

This code will set GridView height as per number of Child.

Note: Where 5 is number of columns.

private void setDynamicHeight(GridView gridView) {
        ListAdapter gridViewAdapter = gridView.getAdapter();
        if (gridViewAdapter == null) {
            // pre-condition
            return;
        }

        int totalHeight = 0;
        int items = gridViewAdapter.getCount();
        int rows = 0;

        View listItem = gridViewAdapter.getView(0, null, gridView);
        listItem.measure(0, 0);
        totalHeight = listItem.getMeasuredHeight();

        float x = 1;
        if( items > 5 ){
            x = items/5;
            rows = (int) (x + 1);
            totalHeight *= rows;
        }

        ViewGroup.LayoutParams params = gridView.getLayoutParams();
        params.height = totalHeight;
        gridView.setLayoutParams(params);
    }

Hope this will help you.

Hiren Patel
  • 52,124
  • 21
  • 173
  • 151
  • 1
    Saving lots of time, Thank you :) – Dharmbir Singh Dec 29 '16 at 13:00
  • What is ListAdapter? – dev1998 Feb 05 '17 at 16:58
  • @dev1998 a default android widget component. Listadapter is an interface as defined in https://developer.android.com/reference/android/widget/ListAdapter.html – Philippe Creytens Mar 01 '17 at 11:06
  • Thanks a lot for this, works like a charm. Made a change to this solution: To get the correct listItem height when there is vertical spacing I've changed totalHeight = listItem.getMeasuredHeight() to totalHeight = listItem.getMeasuredHeight() * gridView.getVerticalSpacing(). This will only work if there actually is vertical spacing though so be aware or check if the vertical spacing is larger than 0. You can't get rid of the hard coded number of columns by using gridView.getNumColumns(). – Peter Eysermans Sep 01 '17 at 20:15
6

Developed on the idea shared by https://stackoverflow.com/users/4233197/hiren-patel, so Thank You :)

Pass you GridView and its number of columns through this function and you are done.

private void setGridViewHeightBasedOnChildren(GridView gridView, int noOfColumns) {
        ListAdapter gridViewAdapter = gridView.getAdapter();
        if (gridViewAdapter == null) {
            // adapter is not set yet
            return;
        }

        int totalHeight; //total height to set on grid view
        int items = gridViewAdapter.getCount(); //no. of items in the grid
        int rows; //no. of rows in grid

        View listItem = gridViewAdapter.getView(0, null, gridView);
        listItem.measure(0, 0);
        totalHeight = listItem.getMeasuredHeight();

        float x;
        if( items > noOfColumns ){
            x = items/noOfColumns;

            //Check if exact no. of rows of rows are available, if not adding 1 extra row
            if(items%noOfColumns != 0) {
                rows = (int) (x + 1);
            }else {
                rows = (int) (x);
            }
            totalHeight *= rows;

            //Adding any vertical space set on grid view
            totalHeight += gridView.getVerticalSpacing() * rows;
        }

        //Setting height on grid view
        ViewGroup.LayoutParams params = gridView.getLayoutParams();
        params.height = totalHeight;
        gridView.setLayoutParams(params);
    }
Utsav Barnwal
  • 985
  • 11
  • 14
  • 1
    Well Explained :) Thanks Dude – Anshul1507 Jul 27 '20 at 21:55
  • Thanks! Using your code I understood that an inner view had zero dimensions. I found that created the view in an adapter improperly. See https://stackoverflow.com/a/63719697/2914140. – CoolMind Sep 03 '20 at 08:31
  • @CoolMind glad it could help :) – Utsav Barnwal Sep 04 '20 at 13:59
  • Big thank you! For others, who might come across this code: please note that for some reasons `gridView.numColumns` returns `-1` even if you specify `android:numColumns` in XML, so that's probably the reason why you need to specify the num of columns manually. – Vasiliy Oct 17 '21 at 10:48
4

This is working for me:

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

        <!-- MORE LAYOUTS IF YOU WANT -->

    <ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:fillViewport="true" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

                <!-- MORE LAYOUTS IF YOU WANT -->

       <GridView
            android:id="@+id/gridview"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginBottom="10dp"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:layout_marginTop="5dp"
            android:gravity="center"
            android:horizontalSpacing="10dp"
            android:numColumns="auto_fit"
            android:stretchMode="columnWidth"
            android:verticalSpacing="10dp" >
       </GridView>

             <!-- MORE LAYOUTS IF YOU WANT -->

    </RelativeLayout>

    </ScrollView>

</RelativeLayout>
Neonigma
  • 1,825
  • 17
  • 20
  • 1
    This will work for height but not useful for pagination. You can not set pagination using `android:fillViewport="true"` in scrollview . – Anand Savjani Jul 29 '16 at 12:20
4

When onMeasure() is called for GridView, if heightMode is MeasureSpec.UNSPECIFIED, then the gridview will be as tall as all of it's contained elements (plus some padding).

To ensure that this is the case, you can make a quick custom view extending GridView and including the following override:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
    }
Graham
  • 993
  • 6
  • 19
1

Graham's solution did not work for me. However this one (with a bit more hacking) did: https://stackoverflow.com/a/8483078/479478

Community
  • 1
  • 1
Eric Chen
  • 3,562
  • 7
  • 39
  • 58
0

This is working for me

public class MyGridView extends GridView {
    public MyGridView(Context context) {
        super(context);
    }

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

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

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

        // set childs height to same value as child that has highest value
        for(int i=0; i<getChildCount(); i++) {
            MyLinearLayout child = (MyLinearLayout) getChildAt(i);
            AbsListView.LayoutParams params = (AbsListView.LayoutParams) child.getLayoutParams();
            params.height = MyLinearLayout.maxHeight;
            child.setLayoutParams(params);
        }

        // calculate total height and apply to the grid
        double numRow = Math.ceil((double) getCount() / (double) getNumColumns());
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) this.getLayoutParams();
        params.height = (int) numRow *  MyLinearLayout.maxHeight;
        this.setLayoutParams(params);

        // invalidate after change grid's height
        this.invalidate();
    }
}

MyLinearLayout class

public class MyLinearLayout extends LinearLayout {
    public static int maxHeight = 0;

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

    public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (maxHeight<this.getMeasuredHeight()) maxHeight=this.getMeasuredHeight();
    }
}

The GridView contains some childs of MyLinearLayout, and this is the layout

<?xml version="1.0" encoding="utf-8"?>
<com.mysystem.view.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="100dp"
              android:layout_height="100dp"
              android:background="#ddd"
              android:padding="0.5dp"
              android:layout_gravity="top"
              android:gravity="center_horizontal"
              android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#fff"
        android:padding="20dp"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/img_menu"
            android:layout_width="30dp"
            android:layout_gravity="center_horizontal"
            android:layout_height="30dp"
            android:layout_marginTop="5dp"/>

        <TextView
            android:id="@+id/tv_menu"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp"
            android:textAlignment="center"
            android:textColor="#000"
            android:text="Title"
            android:textSize="10sp"/>

    </LinearLayout>
</com.mysystem.view.MyLinearLayout>
  • Though there are no accepted answers for this question, there are some with a fair number of upvotes. Is your solution preferable in some way or under some circumstances? If so, please explain. – hBrent Nov 20 '18 at 00:07
  • In my case which is the GridView has childs with different height, none of solutions above work. – Awiyanto Ajisasongko Nov 20 '18 at 01:43
  • This wont work, if you have two MyGridViews displayed at the same time, as it uses static variable, which is used for every instance of MyLinearLayout (idepentent where it is displayed). Plus it will never be reset, when content changes. – Aorlinn Jun 23 '22 at 21:21