Michael Garner
Michael Garner

Reputation: 1092

Android ExpandableListView populated twice

I am using an expandable list view to display options and sub options respectively in a menu. The data coming in is correct, but oddly the listview seems to call the getChild and getChildView twice for each data entry and thus it is added to the view twice. It iterates through and then seems to iterate through again.

Example:

Categories: -Toys -Electronics -Cars -Toys -Electronics -Cars

Expected: Categories: -Toys -Electronics -Cars

I am really new to Android and Java so I am probably making a rookie mistake. I have stepped through to try and find a point where it would be duplicating the data, but all I could find is that getChild/getChildView is being called twice.

Here is the expandableListView adapter class:

    public class ExpandableListAdapter extends BaseExpandableListAdapter {
    private Context context;
    private ArrayList<String> filterHeaderList;
    // child data in format of header title, child title
    private HashMap<String, ArrayList<JSONObject>> filterOptionsMap;

    public ExpandableListAdapter(Context context, ArrayList<String> filterHeaders, HashMap<String, ArrayList<JSONObject>> filterChildListMap) {
        this.context = context;
        this.filterHeaderList = filterHeaders;
        this.filterOptionsMap = filterChildListMap;
    }

    @Override
    public String[] getChild(int groupPosition, int childPosititon) {
        JSONObject childObject = this.filterOptionsMap.get(this.filterHeaderList.get(groupPosition)).get(childPosititon);
        if (childObject != null) {
            Iterator<?> it = childObject.keys();
            String key = null;
            String value = null;
            if (it.hasNext()) {
                key = it.next().toString();
                try {
                    value = childObject.getString(key);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                String[] data = new String[2];
                data[0] = key;
                data[1] = value;
                return data;
            } else {
                return null;
            }
        }
        return null;
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }

    @Override
    public View getChildView(int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
        String childNum = null;
        String childText = null;
        final String[] childData = (String[]) getChild(groupPosition, childPosition);
        if (childData != null) {
            childNum = childData[0];
            childText = childData[1];
        }
        if (convertView == null) {
            LayoutInflater infalInflater = (LayoutInflater) this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = infalInflater.inflate(R.layout.ps_filter_sort_filter_expandable_child, null);
        }

        TextView tvChildTitle = (TextView) convertView.findViewById(R.id.ps_filter_sort_filter_expandable_child_title);

        tvChildTitle.setText(childText);
        tvChildTitle.setTag(R.id.ps_filter_sort_filter_expandable_child_title + groupPosition + childPosition, childNum);
        return convertView;
    }

    @Override
    public int getChildrenCount(int groupPosition) {
        return this.filterOptionsMap.get(this.filterHeaderList.get(groupPosition)).size();
    }

    @Override
    public String getGroup(int groupPosition) {
        return this.filterHeaderList.get(groupPosition);
    }

    @Override
    public int getGroupCount() {
        return this.filterHeaderList.size();
    }

    @Override
    public long getGroupId(int groupPosition) {
        return groupPosition;
    }

    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        String headerTitle = (String) getGroup(groupPosition);
        if (convertView == null) {
            LayoutInflater infalInflater = (LayoutInflater) this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = infalInflater.inflate(R.layout.ps_filter_sort_filter_expandable_parent, null);
        }

        TextView tvGroupTitle = (TextView) convertView.findViewById(R.id.ps_filter_sort_filter_expandable_parent_title);
        tvGroupTitle.setTypeface(null, Typeface.BOLD);
        tvGroupTitle.setText(headerTitle);

        return convertView;
    }

    @Override
    public boolean hasStableIds() {
        return false;
    }

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }
}

I then call it like this:

filterListAdapter = new ExpandableListAdapter(this.getActivity(), filterHeaderList, filterOptionsMap);
        // setting list adapter
        expListView.setAdapter(filterListAdapter);
        filterListAdapter.notifyDataSetChanged();

There are reasons the data is organized in this fashion which I'd rather not get into. What I can say is that the HashMap "filterOptionsMap" is correct and there is no duplication in it or the "filterHeaderList"

Any help would be greatly appreciated. I have not been able to find someone having a similar issue and I've been banging my head on the keyboard over this.

Thank you.

P.S. You also need to tap the group heading twice to get it to collapse, but once to open. I don't know if it is related or expected behavior, but I would hope to have it be a single tap to toggle it open or closed. Bonus points for help with that as well.

As requested here is some additional code related to the HashMap:

private void prepareFilterListData(ArrayList<String> headerList, JSONObject[] categoryList, JSONObject[] sellerList, JSONObject[] typeList) {
    filterHeaderArrayList = headerList;
    filterOptionsMap = new HashMap<String, ArrayList<JSONObject>>();

    // Adding child data
    ArrayList<JSONObject> categories = new ArrayList<JSONObject>();
    if (categoryList != null && categoryList.length > 0) {
        for (JSONObject category : categoryList) {
            categories.add(category);
        }
    }

    ArrayList<JSONObject> sellers = new ArrayList<JSONObject>();
    if (sellerList != null && sellerList.length > 0) {
        for (JSONObject seller : sellerList) {
            sellers.add(seller);
        }
    }

    ArrayList<JSONObject> types = new ArrayList<JSONObject>();
    if (typeList != null && typeList.length > 0) {
        for (JSONObject type : typeList) {
            types.add(type);
        }
    }

    filterOptionsMap.put(filterHeaderArrayList.get(0), categories);
    filterOptionsMap.put(filterHeaderArrayList.get(1), sellers);
    filterOptionsMap.put(filterHeaderArrayList.get(2), types);
}


    filterHeaderList = Directory.getFilterOptions();
    filterCategoryList = Directory.getFilterCategories();
    filterSellerList = Directory.getFilterSellers();
    filterTypeList = Directory.getFilterTypes();

    // Forms data structures from given input
    if (filterHeaderList != null && filterCategoryList != null && filterSellerList != null && filterTypeList != null) {
        prepareFilterListData(filterHeaderList, filterCategoryList, filterSellerList, filterTypeList);


private JSONObject[] getFilterTypes(JSONObject productSearchSettings, String filterTypes) {
    JSONArray typesObj;
    JSONObject[] types;
    try {
        typesObj = productSearchSettings.optJSONArray(filterTypes);
        if (typesObj != null) {
            types = new JSONObject[typesObj.length()];
            for (int i = 0; i < typesObj.length(); i++) {
                String key = "type" + String.valueOf(i);
                String val = (String) typesObj.get(i).toString();
                types[i] = new JSONObject().put(key, val);
            }
            return types;
        } else {
            return null;
        }
    } catch (JSONException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return null;
}

private JSONObject[] getFilterSellers(JSONObject productSearchSettings, String filterSellers) {
    JSONObject sellersObj = null;
    JSONObject[] sellers;
    try {
        sellersObj = productSearchSettings.getJSONObject(filterSellers);
        if (sellersObj != null) {
            sellers = new JSONObject[sellersObj.length()];
            Iterator<?> it = sellersObj.keys();
            int i = 0;
            while (it.hasNext()) {
                String key = (String) String.valueOf(it.next().toString());
                String val = sellersObj.getString(key);
                sellers[i] = new JSONObject().put(key, val);
                i++;
            }
            return sellers;
        } else {
            return null;
        }
    } catch (JSONException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return null;
}

private JSONObject[] getFilterCategories(JSONObject productSearchSettings, String filterCategories) {
    JSONObject categoriesObj = null;
    JSONObject[] categories;
    try {
        categoriesObj = productSearchSettings.getJSONObject(filterCategories);
        if (categoriesObj != null) {
            categories = new JSONObject[categoriesObj.length()];
            Iterator<?> it = categoriesObj.keys();
            int i = 0;
            while (it.hasNext()) {
                String key = (String) String.valueOf(it.next().toString());
                String val = categoriesObj.getString(key);
                categories[i] = new JSONObject().put(key, val);
                i++;
            }
            return categories;
        } else {
            return null;
        }
    } catch (JSONException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return null;

}

As mentioned before, however the hashmap going into the adapter is correct. I verified through debugging.

Upvotes: 3

Views: 3158

Answers (4)

Pedro Gonzalez
Pedro Gonzalez

Reputation: 1459

this worked for me: inside onGroupClick, remove call to parent.expandGroup(groupPosition), and make it return false;

expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
        @Override
        public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
            parent.expandGroup(groupPosition); // delete this
            Log.d(TAG, "onGroupClick: ");
            return false;
        }
    });

Upvotes: 0

marco
marco

Reputation: 1752

I'm sure that the lack of "return true" could be the reason in your system, but since it didn't work for me, I add an answer as I've found a cause:

ExpandableListView doc:

Note: You cannot use the value wrap_content for the android:layout_height attribute of a ExpandableListView in XML if the parent's size is also not strictly specified (for example, if the parent were ScrollView you could not specify wrap_content since it also can be any length. However, you can use wrap_content if the ExpandableListView parent has a specific size, such as 100 pixels.

My ExpandableListView was in fact inside a LinearLayout with height="wrap_content". Set it to match_parent and, voila, problem solved.

I'm not 100% sure but it could be that, with undefined height, the getChildView is called twice because of the re-sizing of the parent.

And it could be (I know it for personal experience, changing several parameters during testing is usually not a good idea as it creates misunderstandings on the causes) that you also set the height to match_parent while you added a return true and thought that the solution was the latter.

It would be helpful for all if you could just unset the height of the container of your ExpandableListView OR set the return to falseand see which is the real cause. If both, well, we've found 2 causes.

Upvotes: 4

Michael Garner
Michael Garner

Reputation: 1092

I finally found it. I really appreciate the feedback and your help.

In the click event for the group, I was not returning true to indicate the click was handled. I was using the click event to toggle the expansion.

I don't know why exactly that would cause duplication and also require two clicks to close the group but toggling that return for the click event on the group made all the difference.

Upvotes: 7

Ifrit
Ifrit

Reputation: 6821

Hmmm...Sorry to say, I'm still not seeing anything to cause that behavior. Generally this could be caused by unknowingly adding new content. Usually because one modifies the data externally from the adapter.

In your case filterHeaderList in the adapter and the list returned by Directory.getFilterOptions() are the same list. So if you modify that Directory.getFilterOptions() list, then you'll also inadvertently modify the adapter. Of course that method could just be generating a new list on the fly, so this may not even be a concern for you.

If you are positive the filterHeaderList is correct during the getChildView and getGroupView invocations, I'm not sure what else could be wrong. The custom group/child views would not cause this.

All I can suggest is resorting to some more aggressive debugging measures. Basically start stripping that Activity/Fragment down to the bare bones. Eliminate as many variables as possible so that you are just doing the most simplest case...loading an ExpandableListView and adapter with some data.

You could also try feeding in some fake data. Just try one child item with one group to see if it's still doubling everything.

Upvotes: 1

Related Questions