Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calculating the height of each row of a ListView

I am trying to make the background of a ListView scroll with the ListView. I am basing my approach on this class in Shelves, but while in Shelves everything has the same height, I can't make the same guarantee.

I have an activity like so:

public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listView = (ListView) findViewById(R.id.listView);

        List<String> items = new ArrayList<String>();
        for(int i=0; i < 100; ++i) {
            items.add("Hello " + i);
        }
        CustomArrayAdapter adapter = new CustomArrayAdapter(this, items);

        listView.setAdapter(adapter);
    }
}

Where CustomArrayAdapter is:

public class CustomArrayAdapter extends ArrayAdapter<String> {
    private List<Integer> mHeights;

    public View getView(int position, View convertView, ViewGroup parent) {
        //snip
    }
}

What I want to do is populate mHeights in the adapter with heights of the rows of the view.

My main attempt was to do this, in getView():

if(row.getMeasuredHeight() == 0) {
    row.measure(
        MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY),
        MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY));
}
mHeights.set(position, row.getMeasuredHeight());

I've tried several ways to do this, but I can't get anything to work.

In getView, if I call getHeight() I get a return value of 0, while if I call getMeasuredHeight(), I get something non-zero but not the real height. If I scroll down and then up again (taking one of the rows out of view) getHeight() == getMeasuredHeight() and both have the correct value. If I just update mHeights in getView as I go (by saying mHeights.set(position, row.getHeight()); it works - but only after I've scrolled to the bottom of the ListView. I've tried calling row.measure() within the getView method, but this still causes getHeight() to be 0 the first time it is run.

My question is this: how do I calculate and populate mHeights with the correct heights of the rows of the ListView? I've seen some similar questions, but they don't appear to be what I'm looking for. The ViewTreeObserver method seems promising, but I can't figure out how to force calls to getView() from it (alternately, to iterate the rows of the ListView). Calling adapter.notifyDataSetChanged() causes an infinite (although non-blocking) loop.

  1. Get Actual Height of View
  2. how to get height of the item in a listview?

This is related to my previous question on the subject, which seems to have missed the point: ListView distance from top of the list

like image 726
mindvirus Avatar asked Oct 30 '12 02:10

mindvirus


2 Answers

I figured out how to do this. It's was a bit tricky, but it works.

The subtlety was getting layout parameters when they are known. As I learned, getView() is returning the view for the initial time, so of course it can't have a height yet - the parent doesn't know about the row yet (since getView()'s job is to inflate it), so we need a callback.

What might that callback be? It turns out, as best I can tell it's the OnLayoutChangeListener() for the row's view. So in getView() in my custom ArrayAdapter, I added a callback for onLayoutChanged like so:

row.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    public void onLayoutChange(View v, int left, int top, int right,
            int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
        ref.removeOnLayoutChangeListener(this);
        Log.d("CustomArrayAdapter", "onLayoutChange(" + position + "): height " + (bottom - top));
        mHeights.set(position, (bottom - top));
        if(position > 0) {
            mDistances.set(position, bottom - top + mDistances.get(position-1) + ((ListView)parent).getDividerHeight());
        }
        holderRef.distanceFromTop = mDistances.get(position);
        Log.d("CustomArrayAdapter", "New height for " + position + " is " + mHeights.get(position) + " Distance: " + mDistances.get(position));
    }
});

Here, row is the view to be returned by getView() in the adapter, mHeights is a list of the height of each element of the ListView, and mDistances is a list of the distance from the top of the list view in pixels - the real thing that I wanted to calculate (that is, if you laid out the entire list view end to end, how far from the top element i would be from the top). All of these are stored within the custom ArrayAdapter. These lists are initialized to zeroes.

Adding this callback allowed me to calculate the height of the view.

My goal was to have a ListView that has a background that scrolls with the list. If anyone is interested, I put the code up on GitHub if anyone would like to review it: https://github.com/mdkess/TexturedListView

like image 115
mindvirus Avatar answered Oct 22 '22 16:10

mindvirus


This SO answer should work for developers who also want to support API level < 11.

Basically, instead of adding a View.onLayoutChnageListener to your view, you can set a OnGlobalLayoutListener on your view's ViewTreeObserver.

like image 21
Hassaan Avatar answered Oct 22 '22 16:10

Hassaan