ExpandableListView expandable list not expanding when its height is wrap_content, and something is below it

10.9k views Asked by At

Here is my layout code. When i click on the expandable list view it does not expand. However, if I increase the height of list it expands. Is there a way to increase the height of list dynamically through code? Can i make it to expand in the middle of my layout. Because if I increase the height, it'll just add unnecessary white space in collapsed list.

Code:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.pocketcash.pocketcash.HomeActivity">

        <Switch
            android:id="@+id/detailedCashInfoSwitch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_alignParentRight="true"
            android:layout_alignParentTop="true"
            android:onClick="cashToggleSwitchClicked"
            android:text="Toggle Detailed Cash Info" />

        <Button
            android:id="@+id/addCashButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/expandableListView2"
            android:layout_centerHorizontal="true"
            android:onClick="launchAddCash"
            android:text="Add cash" />

        <Button
            android:id="@+id/addExpenditureButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/addCashButton"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="25dp"
            android:onClick="launchAddExpenditure"
            android:text="Add expenditure" />

        <TextView
            android:id="@+id/recentLogsText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/addExpenditureButton"
            android:layout_marginTop="50dp"
            android:text="Recent Logs:"
            android:textAppearance="?android:attr/textAppearanceMedium" />

        <TextView
            android:id="@+id/logText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/recentLogsText"
            android:layout_marginTop="0dp"
            android:text="Small Text"
            android:textAppearance="?android:attr/textAppearanceSmall" />

        <TextView
            android:id="@+id/recentViewAllText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignTop="@+id/recentLogsText"
            android:layout_marginLeft="10dp"
            android:layout_toEndOf="@+id/recentLogsText"
            android:layout_toRightOf="@+id/recentLogsText"
            android:onClick="recentViewLogClicked"
            android:text="(view all)"
            android:textAppearance="?android:attr/textAppearanceMedium" />

        <TextView
            android:id="@+id/addViewAllText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/addCashButton"
            android:layout_centerHorizontal="true"
            android:onClick="addViewLogClicked"
            android:text="(view logs)"
            android:textAppearance="?android:attr/textAppearanceSmall" />

        <TextView
            android:id="@+id/expenseViewAllText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/addExpenditureButton"
            android:layout_centerHorizontal="true"
            android:onClick="expenseViewLogClicked"
            android:text="(view logs)"
            android:textAppearance="?android:attr/textAppearanceSmall" />

        <ImageView
            android:id="@+id/userImage"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/detailedCashInfoSwitch"
            android:layout_marginTop="34dp"
            android:background="@drawable/default_user" />

        <TextView
            android:id="@+id/nameText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignTop="@+id/userImage"
            android:layout_marginLeft="25dp"
            android:layout_toEndOf="@+id/userImage"
            android:layout_toRightOf="@+id/userImage"
            android:text="Name"
            android:textAppearance="?android:attr/textAppearanceLarge" />

        <TextView
            android:id="@+id/logoutText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@+id/nameText"
            android:layout_alignStart="@+id/nameText"
            android:layout_below="@+id/nameText"
            android:clickable="true"
            android:onClick="logUserOut"
            android:text="logout"
            android:textAppearance="?android:attr/textAppearanceSmall" />

        <ImageButton
            android:id="@+id/settingsButton"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_alignTop="@+id/nameText"
            android:layout_marginLeft="25dp"
            android:layout_toEndOf="@+id/nameText"
            android:layout_toRightOf="@+id/nameText"
            android:background="@drawable/settingsicon"
            android:onClick="launchSettingsActivity" />

        <fragment
            android:id="@+id/fragment_currentBalanceInfo"
            android:name="com.pocketcash.pocketcash.CurrentBalanceInfoFragment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/userImage"
            android:layout_centerHorizontal="true"
            tools:layout="@layout/fragment_current_balance_info" />

        <ExpandableListView
            android:id="@+id/expandableListView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/fragment_currentBalanceInfo"
            android:layout_centerHorizontal="true"
            android:clickable="true" />

    </RelativeLayout>
</ScrollView>
7

There are 7 answers

2
Jai Saxena On BEST ANSWER

After much research on this topic, i could only reach the conclusion that a scrollable object like the expandable listview cant be declared within another scrollable view. However i tweaked around with my java code a little bit for solving this issue during runtime.

    expListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
        @Override
        public void onGroupExpand(int groupPosition) {
            int height = 0;
            for (int i = 0; i < expListView.getChildCount(); i++) {
                height += expListView.getChildAt(i).getMeasuredHeight();
                height += expListView.getDividerHeight();
            }
            expListView.getLayoutParams().height = (height+6)*10;
        }
    });

    // Listview Group collapsed listener
    expListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {

        @Override
        public void onGroupCollapse(int groupPosition) {
            expListView.getLayoutParams().height = 61;
        }
    });

This way i adjusted the height of the widget dynamically when it was expanded or collapsed. Its not a dependable method and it works on hit and trial so i ultimately had to move this widget to a completely new activity. However this did work flawlessly. But if anyone finds a better answer, please do share.

5
Kartheek On

As Romain Guy (Google Engineer works on UI toolkit) Said in his post

By setting the width to wrap_content you are telling ListView to be as wide as the widest of its children. ListView must therefore measure its items and to get the items it has to call getView() on the Adapter. This may happen several times depending on the number of layout passes, the behavior of the parent layout, etc.

So if you set the layout width or layout height of your ListView to wrap_content the ListView will try to measure every single view that is attached to it - which is definitely not what you want.

Keep in mind: avoid setting wrap_content for ListViews or GridViews or ExpandableListView at all times, for more details see this Google I/O video talking about the world of listview

So Make ExpandableListView to match_parent

0
Doogy1st On

I know the question was already answered but I have another solution which could work for more cases: I set the initial height of the list view by counting the number of items, and then update this height on collapse/expand of group by retrieving children count of the expanded/collapsed group. Here is a sample code:

int item_size = 50;  // 50px
int sub_item_size = 40;  // 40px
expListView.getLayoutParams().height = item_size*myAdapter.getGroupCount();
expenseListView.setAdapter(myAdapter);
// ListView Group Expand Listener
expenseListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
    @Override
    public void onGroupExpand(int groupPosition) {
        int nb_children = myAdapter.getChildrenCount(groupPosition);
        expenseListView.getLayoutParams().height += sub_item_size*nb_children;
    }
});

// Listview Group collapsed listener
expenseListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
    @Override
    public void onGroupCollapse(int groupPosition) {
        int nb_children = myAdapter.getChildrenCount(groupPosition);
        expenseListView.getLayoutParams().height -= sub_item_size*nb_children;
    }
});
0
winubmac1024 On

Unfortunately, there doesn't seem to be a way to use wrap_content as the height of the ExpandableListView. Instead, setting a fixed height and android:nestedScrollingEnabled="true" worked for me.

0
soshial On

Normally we can't use ListView, GridView or ExpandableListView inside Scrollview. If you need it, then you have to give fixed height to your expandable ListView.

private void setListViewHeight(final ExpandableListView listView, final int groupPosition) {
    final ExpandableListAdapter listAdapter = (ExpandableListAdapter) listView.getExpandableListAdapter();
    int totalHeight = 0;
    // be careful with unspecified width measure spec, it will ignore all layout params and even
    // screen dimensions, so to get correct height, first get maximum width View can use and
    // call measure() this way
    final int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.EXACTLY);
    for (int i = 0; i < listAdapter.getGroupCount(); i++) {
        final View groupItem = listAdapter.getGroupView(i, false, null, listView);
        groupItem.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);

        totalHeight += groupItem.getMeasuredHeight();
        // count only expanded other groups or the clicked collapsed one
        if (((listView.isGroupExpanded(i)) && (i != groupPosition))
                || ((!listView.isGroupExpanded(i)) && (i == groupPosition))) {
            for (int j = 0, childrenNumber = listAdapter.getChildrenCount(i); j < childrenNumber; j++) {

                final View listItem = listAdapter.getChildView(i, j, j == childrenNumber - 1, null, listView);
                listItem.measure(desiredWidth, View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
                totalHeight += listItem.getMeasuredHeight();
            }
        }
    }
    final ViewGroup.LayoutParams params = listView.getLayoutParams();
    final int height = totalHeight
            + (listView.getDividerHeight() * (listAdapter.getGroupCount() - 1));
    params.height = height;
    listView.setLayoutParams(params);
    listView.requestLayout();
}

and assign this method on list group click:

expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
    @Override
    public boolean onGroupClick(final ExpandableListView expandableListView, final View view, final int groupPosition, final long id) {
        setListViewHeight(expandableListView, groupPosition);
        return false;
    }
});

IMPORTANT: although this code should work in most cases, I had some problems when child list items were TextViews with android:layout_height="wrap_content", hence their height depended on their width, which might be connected to this problem: ViewGroup{TextView,...}.getMeasuredHeight gives wrong value is smaller than real height. Please, write a comment if you manage to fix it.

0
Er. Harsh Rathore On

I found a way better solution to achieve this phenomena with the following less complex and much efficient Kotlin code.

before all keep some points in mind :

  1. Used fixed height Parent and Child views.
  2. Extracted the heights of both Parent and Child into dimens.xml file.
  3. Set divider='@null', scrollbars="none", groupIndicator="@null" (for customGroupIndicator, otherwise ignore) in ExpandableListView.
  4. Never use paddingVertical including paddingTop and paddingBottom, replace them with layoutMargins for views having dynamic height.

Code Logic (For me this chunk is a part of a ViewModel Function named initUi(uiObject))

//Here `ui` is a DataBinding object.
//You can use ViewBinding or simple Android findViewById as per your comfort.

val data: List<ModelClass> = listObject
val context = ui.root.context

ui.expandableList.apply {
    val mAdapter = MyExpandableListAdapter(data, context)
    setAdapter(mAdapter)

    //fetching parent height from dimens.xml
    val parentViewHeight = ceil(
        context.resources.getDimension(R.dimen.layout_menu_parent_height)
    ).toInt()

    //fetching child height from dimens.xml
    val childViewHeight = ceil(
        context.resources.getDimension(R.dimen.layout_menu_child_height)
    ).toInt()

    //Setting up the initial height of view when All parents are collapsed
    layoutParams.height = data.size * parentViewHeight

    setOnGroupExpandListener { groupPosition ->
        //Increasing the height for Expanded SubCategory
        layoutParams.height += mAdapter.getChildrenCount(groupPosition) * childViewHeight
    }

    setOnGroupCollapseListener { groupPosition ->
        //Decreasing the height for Collapsed SubCategory
        layoutParams.height -= mAdapter.getChildrenCount(groupPosition) * childViewHeight
    }
}

MyExpandableListAdapter.kt

class FragmentMenuExpandableListAdapter(
    val data: List<Data>, val context: Context
): BaseExpandableListAdapter() {
    //Declaring inflater Once can save a lot of CPU and RAM uses... 
    private val inflater = LayoutInflater.from(context)

    override fun getGroupCount(): Int = data.size

    override fun getChildrenCount(groupPosition: Int): Int = data[groupPosition].subcategories.size

    override fun getGroup(groupPosition: Int): Data = data[groupPosition]

    override fun getChild(groupPosition: Int, childPosition: Int) = data[groupPosition].subcategories[childPosition]

    override fun getGroupId(groupPosition: Int): Long = groupPosition.toLong()

    override fun getChildId(groupPosition: Int, childPosition: Int): Long = "$groupPosition$childPosition0".toLong()

    override fun hasStableIds(): Boolean = true

    override fun getGroupView(
        groupPosition: Int,
        isExpanded: Boolean,
        convertView: View?,
        parent: ViewGroup
    ): View {
        val binding = LayoutMenuParentBinding.inflate(inflater, parent, false)
        binding.apply {
            val temp = getGroup(groupPosition)
            title.text = temp.category_name

            //here I used a custom ImageView as GroupIndicator
            //So following code chunk is Optional
            if(temp.subcategories.isEmpty()) dropdownIcon.visibility = View.GONE
            else dropdownIcon.let { icon ->
                val view = parent as ExpandableListView
                icon.setImageResource(
                    if(view.isGroupExpanded(groupPosition)) {
                        icon.setOnClickListener { view.collapseGroup(groupPosition) }
                        R.drawable.ic_state_expanded
                    }
                    else {
                        icon.setOnClickListener {
                            view.expandGroup(groupPosition)
                        }
                        R.drawable.ic_state_collapsed
                    }
                )
            }

            root.setOnClickListener {
                TODO("Do on Parent click")
            }
        }
        return binding.root
    }

    override fun getChildView(
        groupPosition: Int,
        childPosition: Int,
        isLastChild: Boolean,
        convertView: View?,
        parent: ViewGroup?
    ): View {
        val binding = LayoutMenuChildBinding.inflate(
            inflater, parent, false
        ).apply {
            val temp = getChild(groupPosition, childPosition)
            title.text = temp.categoryName

            root.setOnClickListener {
                TODO("Do on Child click")
            }
        }
        return binding.root
    }

    override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean = true
}
0
Sonali Kamble On

in my case, wrap_content and match_parent have not worked. I got a solution that works for all screen resolutions. Try adding a custom expandable listview as below and use that view in your XML file.

public class CustomExpandableListview extends ExpandableListView { 
   
     public CustomExpandableListview(Context context, AttributeSet attrs) { 
            super(context, attrs);        
 }       

@Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
         int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);       
    super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);             
    ViewGroup.LayoutParams params = getLayoutParams();            
    params.height = getMeasuredHeight();         
   } 
}

<com.ui.CustomExpandableListview     
android:id="@+id/device_break_down_list"     
android:layout_width="match_parent"     
android:layout_height="wrap_content"     
android:groupIndicator="@null" />