Wednesday, December 8, 2010

Android: put ListView in a ScrollView

When I need to use ListView with other controls on the same screen, and it doesn't fit the screen vertically, I wish to get ScrollView over all controls, to scroll entire screen. But, that's the point when "shit happens".

Android core developers say you "must not put ListView inside ScrollView", because of many reasons, one of them - ListView has it's own scroll.

When you try just do it, ListView collapses, and you wish it not to be collapsed at all.

To accomplish that, I use a "hack", measuring and setting the height directly:

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

        int totalHeight = 0;
        int desiredWidth = MeasureSpec.makeMeasureSpec(listView.getWidth(), MeasureSpec.AT_MOST);
        for (int i = 0; i < listAdapter.getCount(); i++) {
            View listItem = listAdapter.getView(i, null, listView);
            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();
    }
}

Call this function right after you change ListView items, like that:

Utility.setListViewHeightBasedOnChildren(myListView);

Updated 9 Feb. 2011: fixed one nasty bug :) It all works now!

P.S. Original code is written by DougW at stackoverflow.com, I've modified it a bit.

51 comments:

  1. I don't mind you modifying and reusing my public work, but a little credit would be nice.

    http://stackoverflow.com/questions/3495890/how-can-i-put-a-listview-into-a-scrollview-without-it-collapsing/3495908#3495908

    ReplyDelete
  2. Hi, nex
    Thanks for your code, that works like a charm!
    But I've a problem when I use it in my program: when there's just 1 list, that works fine; when there're 3 lists in the scrollable, I can scroll it while the second and third lists don't show normally..There is a enorme space between the second and third list, and after the second list.
    I debugged the code and the problem comes from :
    listItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
    totalHeight += listItem.getMeasuredHeight();
    The return value of the height is a very very large number.
    I can't figure out how to resolve the problem, do you have any ideas?
    Thanks !

    ReplyDelete
  3. DougW, sure!
    And thanks for original code, man!

    ReplyDelete
  4. xue, I don't have any clue for your case, maybe there is a problem in layout?

    ReplyDelete
  5. I tried this on HTC desire, list is getting displayed but its(listview's) height is increased by 3 times. Could you please post your layout xml file here?

    ReplyDelete
  6. Call this function right after you change ListView items, like that:

    -> but I had a problem. each item's height was measured as too high. I managed to solve. like this.


    listview.postDelayed(new Runnable() {
    public void run() {
    Utility.setListViewHeightBasedOnChildren(listview);
    }
    }, 400);


    Anyway, thanks for the code!!! ^^

    ReplyDelete
  7. Thanks to all of you guys!
    had the same problem as cosmos, but you did find the solution. Works perfekt for me!

    ReplyDelete
  8. This solution is still inaccurate - in a list of 30 items, it cuts the last one off for me.

    ReplyDelete
  9. Artem, are you sure there is nothing wrong with your layout?
    If not, maybe you will find a fix?

    ReplyDelete
  10. Hey man...your a genius...totally solved my problem!

    ReplyDelete
  11. Artem, I had the same issue. Found the problem in my adapter, the adapter did not assign text at the correct position of the list. So it either cut the last one off or added white space at the end. After I solved this the height was as desired. I suppose the reason is that my list items have variable height. When the user scrolls a longer text komms at the wrong position making an item longer as it should actually be and therefore longer as calculated.

    Cheers

    ReplyDelete
  12. This doesn't work for me (Android 3.2). It draws the first 2 rows, and nothing else. Still, debugging, the height calculation is correct, as it goes through all of the adapter elements (so it's not an adapter issue).
    Anyone else has got this problem? Can anyone solve it?

    ReplyDelete
  13. I've been bashing my head trying to get a list inside a scroller to work (I'm just a few days into learning android..) and this code worked perfectly for me, thank you so much!

    ReplyDelete
  14. Great tip! Anyone tried to adapt it for GridView? it seems GridView are missing important methods for that: getnumcolumns is only available from API level 11 and no way to get vertical spacing :(

    ReplyDelete
  15. Yay :) thank you so much! Spent so long struggling with this recently and now it's all working fine! :)

    ReplyDelete
  16. I always get nullpointerexceptions in View.measure. Is there a specific point where or time when I should make this call?

    ReplyDelete
    Replies
    1. I have also a nullpointerexception, do you find solution ? anyone else ?

      Delete
    2. I do get NPE as well while executing the measure method.

      Delete
    3. Me too:

      java.lang.NullPointerException at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:493)

      On Jelly Bean 4.2.

      Delete
  17. Thank you so much.. it worked great.. but if I want to restrict the height of listview displayed? I tried doing some modification in the code above but no luck.. Please help me in this. Thanks in advance..

    ReplyDelete
  18. RahulS, try to change "MeasureSpec.UNSPECIFIED" parameter to your desired height.

    ReplyDelete
  19. This code works fine, i have a problem and it is that when i excute it the layout start showing content from the listview and below but my view used to start from the top

    ReplyDelete
    Replies
    1. hey - did u ever find a solution to this problem?

      Delete
    2. I have the same issue. On some devices it jumps past the first content down to the listview.

      Delete
    3. Way to solve this is to pick any widget in your layout. probably the first ones and do:

      widget.setFocusableInTouchMode(true);
      widget.requestFocus();

      (where widget could be anythinf you choose in your layout)

      Delete
  20. use listItem.measure(0, 0); rather than listItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED); for remove unwanted space among controls.

    ReplyDelete
  21. Are you still getting scroll events? I seem to get them, but they always say I'm at the bottom of the list when I haven't even scrolled at all

    ReplyDelete
  22. This may be helpful to some people. I have the same issue, but am using Expandable lists. So, I modified the above to handle that:

    package com.postmaster;

    import android.view.View;
    import android.view.View.MeasureSpec;
    import android.view.ViewGroup;
    import android.widget.ExpandableListAdapter;
    import android.widget.ExpandableListView;

    public class Utility {
    public static void setListViewHeightBasedOnChildren(ExpandableListView listView, int groupid) {
    ExpandableListAdapter listAdapter = listView.getExpandableListAdapter();
    if (listAdapter == null) {
    // pre-condition
    return;
    }

    int totalHeight = 0;
    int desiredWidth = MeasureSpec.makeMeasureSpec(listView.getWidth(), MeasureSpec.AT_MOST);
    for (int i = 0; i < listAdapter.getGroupCount(); i++) {//Get the number of groups
    //Add space for all of the groups
    View groupItem = listAdapter.getGroupView(i, false, null, null);
    groupItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
    totalHeight += groupItem.getMeasuredHeight();
    /* Need to add space for all of the children. If the group was just clicked,
    * it won't already be expanded if we're expanding it. Vice-versa, if we're
    * closing it, it will be expanded. So, we will add the space for the children
    * if it's expanded and not the one clicked, or if it's not already expanded
    * and is the one clicked.
    *
    */
    if(
    ((listView.isGroupExpanded(i)) && (i!= groupid)) ||
    ((!listView.isGroupExpanded(i)) && (i== groupid))
    ){
    for(int j = 0; j < listAdapter.getChildrenCount(i); j++){
    View listItem = listAdapter.getChildView(i, j, false, null, null);
    listItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
    totalHeight += listItem.getMeasuredHeight();
    }
    }
    }

    ViewGroup.LayoutParams params = listView.getLayoutParams();
    int height = totalHeight + (listView.getDividerHeight() * (listAdapter.getGroupCount() - 1));
    if (height < 10)
    height = 200;
    params.height = height;
    listView.setLayoutParams(params);
    listView.requestLayout();
    }
    }

    I didn't change the original function name since I didn't use normal listviews, so change it if you are using both.

    ReplyDelete
  23. I use the above when one of the group's headers has been clicked. Pass in the id of the group header that has been clicked. i.e.:
    listView.setOnGroupClickListener(new OnGroupClickListener()
    {

    @Override
    public boolean onGroupClick(ExpandableListView arg0, View arg1, int arg2, long arg3)
    { Utility.setListViewHeightBasedOnChildren(arg0,arg2);
    });

    ReplyDelete
  24. It's working just the way i wanted. Great job!

    ReplyDelete
  25. TOTALLY GREAT, THANK YOU SO MUCH FROM SPAIN =)

    ReplyDelete
  26. It's working, thank you very much, both of you guys :D

    ReplyDelete
  27. Hello,
    I have a problem to implement this. As when I call adapter's notifyDataSetChanged then the listview doesn't scroll to the end but when I set the EndlessAdapter to the listview, it is working fine. Please suggest me why it doesn't scroll when we call notifyDataSetChanged.

    ReplyDelete
  28. hey i got this error


    09-07 11:56:30.100: ERROR/AndroidRuntime(657): FATAL EXCEPTION: main
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): java.lang.NullPointerException
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:428)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at android.view.View.measure(View.java:8171)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at com.amitech.hashchat.utill.TweetView$Utility.setListViewHeightBasedOnChildren(TweetView.java:1072)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at com.amitech.hashchat.utill.TweetView$getTweet.onPostExecute(TweetView.java:705)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at com.amitech.hashchat.utill.TweetView$getTweet.onPostExecute(TweetView.java:1)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at android.os.AsyncTask.finish(AsyncTask.java:417)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at android.os.AsyncTask.access$300(AsyncTask.java:127)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:429)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at android.os.Handler.dispatchMessage(Handler.java:99)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at android.os.Looper.loop(Looper.java:123)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at android.app.ActivityThread.main(ActivityThread.java:4627)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at java.lang.reflect.Method.invokeNative(Native Method)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at java.lang.reflect.Method.invoke(Method.java:521)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
    09-07 11:56:30.100: ERROR/AndroidRuntime(657): at dalvik.system.NativeStart.main(Native Method)

    any one has solustion

    ReplyDelete
  29. Is it work for Arrayadapter?
    Must we use CustomAdapater?Plz help me.
    any one has solution.

    ReplyDelete
  30. This comment has been removed by the author.

    ReplyDelete
  31. Great it solve my problem.

    Thanks a lot

    ReplyDelete
  32. I use it,but listview onScrollListener donot work.
    who can tell me why?
    thanks

    ReplyDelete
  33. Awesome man You saved my time. Thanks a Lot man.

    ReplyDelete
  34. This solution DID NOT work for me when I have a view with children that wrapped across multiple "lines".

    Instead I found this worked: http://stackoverflow.com/a/5720141/333427

    Here's the contents to simplify your life!

    1) Create a new class that extends ListView:

    package com.example.android.views;

    import android.content.Context;
    import android.graphics.Canvas;
    import android.util.AttributeSet;
    import android.widget.ListView;

    public class ExpandedListView extends ListView {

    private android.view.ViewGroup.LayoutParams params;
    private int old_count = 0;

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

    @Override
    protected void onDraw(Canvas canvas) {
    if (getCount() != old_count) {
    old_count = getCount();
    params = getLayoutParams();
    params.height = getCount() * (old_count > 0 ? getChildAt(0).getHeight() : 0);
    setLayoutParams(params);
    }

    super.onDraw(canvas);
    }

    }

    2)Inside your xml layout file:

    <com.example.android.views.ExpandedListView
    android:id="@+id/list"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:scrollbars="none"
    android:padding="0px"
    />

    ReplyDelete
  35. This comment has been removed by the author.

    ReplyDelete
  36. This is not working for my side can u please tell me why...My layout is like
















    ReplyDelete
  37. I have the following structure













    But leave me much space under the Listview, maybe about 10 cms before displaying the TextView, some solution.

    Here the full xml

    http://pastebin.com/PAx50nZv

    ReplyDelete
  38. I am having same problem of height...after last item in the list scroll appears to nothing...Is there problem with layout or somthing different went wrong?

    ReplyDelete
    Replies
    1. Solved issue of extra height of listview...just change int desiredWidth = MeasureSpec.makeMeasureSpec(listView.getWidth(),
      MeasureSpec.UNSPECIFIED); by int desiredWidth = MeasureSpec.makeMeasureSpec(listView.getWidth(),
      MeasureSpec.EXACTLY);

      UNSPECIFIED is replaced by EXACTLY
      thats it...

      Delete
  39. its working fine but i have problem that.. footer menu also changing the position... can you tell where i need to change the code

    ReplyDelete
  40. Its Awesome Thanks a lots ....:)

    ReplyDelete