Android Listview Measure Height
Asked Answered
C

4

4

I'm trying to find a way to set the height of a listview based on the height of its children items. I followed the solution given here: How can I put a ListView into a ScrollView without it collapsing?

public void setListViewHeightBasedOnChildren(ListView listView) {
    ListAdapter listAdapter = listView.getAdapter();
    if (listAdapter == null) {
        // pre-condition
        return;
    }

    int totalHeight = 0;

    for (int i = 0; i < listAdapter.getCount(); i++) {
        View listItem = listAdapter.getView(i, null, listView);

        if(listItem != null){
            listItem.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT));                
            listItem.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            //listItem.measure(0, 0);
            totalHeight += listItem.getMeasuredHeight();
        }
    }

    ViewGroup.LayoutParams params = listView.getLayoutParams();
    params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
    listView.setLayoutParams(params);
    listView.requestLayout();
}

Now I call the above function on the listview like this:

public void displayReviews(ArrayList<Reviews> resultReviews){
    // Hide the loading progress
    hideReviewsLoading();

    if(resultReviews != null && resultReviews.size() > 0){
        mCurrentReviewList.onFetchFinished(resultReviews);
        setListViewHeightBasedOnChildren(mCurrentReviewList.getListView());
    }
    else{
        // Display a generic text to indicate no reviews are in yet
        displayEmptyText();
    }
}

Here above the mCurrentreviewList is a ListFragment which basically has an adapter to set the elements within a layout.

The problem I'm having is that the height of each listitem that it measures is not accurate. And so in the end when all the list items(reviews) are populated the overall listview that contains it, never fully displays all the list items. It cuts off somewhere below - Like only shows 7.5 reviews out of a 10 total.

I'm not sure what I'm doing incorrectly. Any help and direction would be greatly appreciated!

Crossman answered 7/2, 2014 at 6:27 Comment(3)
Are you sure a ListView is really what you want here ?Icily
@fiddler Yeah, I think a ListView is the best solution I have for the layout I'm looking to implement. Here is a rudimentary layout I drew to explain what I'm trying to achieve. dropbox.com/s/6mhf2xfqwkz2zsi/2014-02-06%2022.56.35.jpg I'm using a TabHost and each Tab initiates a fragment with the FragmentList I described above. So, I don't need a listview which actually scrolls in that tiny window. An example of the implementation can be found in the 500px app. (Screenshot: dropbox.com/s/h4q5poiqfvdpmwa/2014-02-07%2006.57.33.png) and also in google plus app.Crossman
Ok looks like I was able to figure out why the measurement was inaccurate. For ListItem's with variable/dynamic height, this will not work out. The measurement has to know at least one variable and in this case it will be the constant width. So, first calculate the desired width of the listview which you know is going to be constant, by using: int desiredWidth = MeasureSpec.makeMeasureSpec(listView.getWidth(), MeasureSpec.AT_MOST); and then pass it to the listitem measureCrossman
C
32

Ok looks like I was able to figure out why the measurement was inaccurate. I was trying to get a measurement of the listview's height by passing

listItem.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

For ListItem's with variable/dynamic height, this will not work out. The measurement has to know at least one variable and in this case it will be the constant width. So, first calculate the desired width of the listview which you know is going to be constant, by using:

int desiredWidth = MeasureSpec.makeMeasureSpec(listView.getWidth(), MeasureSpec.AT_MOST);

and then pass it to the listitem measure. Full code is given below:

public void setListViewHeightBasedOnChildren(ListView listView) {
    ListAdapter listAdapter = listView.getAdapter();
    if (listAdapter == null) {
        // pre-condition
        return;
    }

    int totalHeight = listView.getPaddingTop() + listView.getPaddingBottom();
    int desiredWidth = MeasureSpec.makeMeasureSpec(listView.getWidth(), MeasureSpec.AT_MOST);
    for (int i = 0; i < listAdapter.getCount(); i++) {
        View listItem = listAdapter.getView(i, null, listView);

        if(listItem != null){
            // This next line is needed before you call measure or else you won't get measured height at all. The listitem needs to be drawn first to know the height.
            listItem.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT));
            listItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
            totalHeight += listItem.getMeasuredHeight();

        }
    }

    ViewGroup.LayoutParams params = listView.getLayoutParams();
    params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
    listView.setLayoutParams(params);
    listView.requestLayout();
}

Please Note This is a hackish implementation and isn't the recommended way. Please see How can I put a ListView into a ScrollView without it collapsing? and understand exactly what you are doing before implementing this.

Crossman answered 7/2, 2014 at 22:20 Comment(7)
I had to change to int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.EXACTLY);Kidron
Wow this just made my day! I was measuring with 0,0 and that was giving me a bogus height, however as soon as I changed it to use the actual width and AT_MOST it worked perfectly!!Ogdon
Great piece of code, so far this was only working for my listview which has a variable height, indeed it measures perfectly with the width specified at the beginning. Thank!Peebles
@Crossman - I have hit an issue. When my app first loads, it measures ok and sets the necessary height. However if my activity is recreated, my ListView triples in height, but it contains the same number of items. I noticed if I delay with a timer the creation of my ListView (500ms), the problem goes away, and I get the correct height again on recreation of the activity. I am wondering why? By the way it is a fragment activity within a tab.Peebles
Not sure why, but you can try calling the piece of code that does the remeasuring(eg setListViewHeightBasedOnChildren function) in the onActivityCreated function of your fragment.Crossman
Yes..My problem solved by important line - listItem.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT)); don`t miss this line guys.Interflow
Problem solved with EXACTLY...But If I use one Listview below other Listview ,then some amount of whitspace is showing between two Listviews! What is the issue? Thanks in advance!Weevil
G
1

ListView is supposed to scroll - so you can, in theory, have infinite number of items in it. No need to set the length of the ListView based on how many items are inside it.

If you do need to set the length, then that defeats the purpose of having a ListView. Please use some other view like Vertical ScrollView or something for what you require.

Gadson answered 7/2, 2014 at 6:34 Comment(3)
Thanks for the reply HariKris, I realize a ListView's primary function is to act as a collector of a list of items which can be scrolled. I'm not sure how to implement a list of items and have a separator by using a listadapter like I'm doing now by just using LinearLayouts. Check my above comment to fiddler to see what layout I'm trying to implement. Is there a way to achieve the same without using a ListView?Crossman
I think ListView is what u need. Just make sure that you allot the remaining space of the screen in your layout to the ListView. And then you add as many review items inside it as you have to. The ListView will scroll naturally. No need to assign fix size to ListView. Whatever screen is left after laying out remaining parts of the screen should be used by ListView - make sure of that in your xml file.Heriot
Yeah right now if i basically don't call the 'setListViewHeightBasedOnChildren' function to reset the listview height, it will behave as you describe. I've allocated the remaining space to the height of the listview. But it is about just half the screen size of my phone(and i'm using a 4.7" phone). On a smaller phone, the window for the listview is going to be very small and the user will need to keep scrolling to read a single item.(probably just 4-6 lines at a time.) It is definitely not going to look good. 500px and google plus have implemented it well, and surely other apps. Not sure howCrossman
E
0

I don't have enough reputation to comment but regarding falc0nit3's answer, I had this implemented but eventually ran into an out of memory error with large cells that have an image etc.

This is because of the first line View listItem = listAdapter.getView(i, null, listView); of the for loop.

Passing null in as the convertView parameter will lead to the adapter inflating each row in your list. This is not ideal, so instead pass in an already inflated row so that the getView only has to replace the contents inside that row (e.g. inflate the first row of the list yourself and continue to use that). See under 'View Recycling' in the following link http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/

Eozoic answered 2/12, 2015 at 12:18 Comment(0)
W
0

falc0nit3's answer (the accepted one) works for me, however I suggest adding the following code right after the for loop if you use list dividers

totalHeight += listView.getDividerHeight() * i;

So the final code snippet would look like this:

public static void setListViewHeightBasedOnChildren(ListView listView) {
    ListAdapter listAdapter = listView.getAdapter();
    if (listAdapter == null)
        return;

    int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.UNSPECIFIED);
    int totalHeight = 0;
    View view = null;
    int i;
    for (i = 0; i < listAdapter.getCount(); i++) {
        view = listAdapter.getView(i, view, listView);
        if (i == 0)
            view.setLayoutParams(new ViewGroup.LayoutParams(desiredWidth, ViewGroup.LayoutParams.WRAP_CONTENT));

        view.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
        totalHeight += view.getMeasuredHeight();

    }

    //add divider height to total height as many items as there are in listview
    totalHeight += listView.getDividerHeight()*i;
    ViewGroup.LayoutParams params = listView.getLayoutParams();
    params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
    listView.setLayoutParams(params);
}
Whitsunday answered 24/8, 2016 at 13:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.