13

I'm looking to write my own ExpandableListAdapter which operates similarly to ArrayAdapter. My data model is this:

public class Group {

    private String name;   

    private List<Child> children;
}

public class Child {

     private String name;
}

Pretty simple. How can I map this relationship into a ExpandableListAdapter implementation? I have a working SimpleExpandableListAdapter working right now, but I need more custom control over items (displaying icons, etc.). What should I do for this?

The main thing is that I need an add() method to be able to add groups and to invalidate the list when children are added and removed from the adapter. I'm actually surprised that there isn't an implementation in the SDK (even an abstract one) which helps one accomplish this.

Naftuli Kay
  • 87,710
  • 93
  • 269
  • 411

4 Answers4

26

Here's an implementation I just whipped up. I have no idea if it works or not, but seems "smart" to me :) By the way, how should one go about getting the combined child id or the combined group id, so I just kinda improvised there.

package example;

import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;

import android.content.Context;
import android.database.DataSetObservable;
import android.database.DataSetObserver;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ExpandableListAdapter;

public abstract class AbstractExpandableListAdapter<A, B> implements ExpandableListAdapter {

    private final List<Entry<A, List<B>>> objects;

    private final DataSetObservable dataSetObservable = new DataSetObservable();

    private final Context context;

    private final Integer groupClosedView;

    private final Integer groupExpandedView;

    private final Integer childView;

    private final LayoutInflater inflater;

    public AbstractExpandableListAdapter(Context context, int groupClosedView, 
            int groupExpandedView, int childView, List<Entry<A, List<B>>> objects) {
        this.context = context;
        this.objects = objects;
        this.groupClosedView = new Integer(groupClosedView);
        this.groupExpandedView = new Integer(groupExpandedView);
        this.childView = new Integer(childView);

        this.inflater = (LayoutInflater) this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public void add(Entry<A, List<B>> group) {
        this.getObjects().add(group);
        this.notifyDataSetChanged();
    }

    public void remove(A group) {
        for (Entry<A, List<B>> entry : this.getObjects()) {
            if (entry != null && entry.getKey().equals(group)) {
                this.getObjects().remove(group);
                this.notifyDataSetChanged();
                break;
            }
        }
    }

    public void remove(Entry<A, List<B>> entry) {
        remove(entry.getKey());
    }

    public void addChild(A group, B child) {
        for (Entry<A, List<B>> entry : this.getObjects()) {
            if (entry != null && entry.getKey().equals(group)) {
                if (entry.getValue() == null) 
                    entry.setValue(new ArrayList<B>());

                entry.getValue().add(child);
                this.notifyDataSetChanged();
                break;
            }
        }
    }

    public void removeChild(A group, B child) {
        for (Entry<A, List<B>> entry : this.getObjects()) {
            if (entry != null && entry.getKey().equals(group)) {
                if (entry.getValue() == null)
                    return;

                entry.getValue().remove(child);
                this.notifyDataSetChanged();
                break;
            }
        }
    }

    public void notifyDataSetChanged() {
        this.getDataSetObservable().notifyChanged();
    }

    public void notifyDataSetInvalidated() {
        this.getDataSetObservable().notifyInvalidated();
    }

    public void registerDataSetObserver(DataSetObserver observer) {
        this.getDataSetObservable().registerObserver(observer);
    }

    public void unregisterDataSetObserver(DataSetObserver observer) {
        this.getDataSetObservable().unregisterObserver(observer);
    }

    public int getGroupCount() {
        return getObjects().size();
    }

    public int getChildrenCount(int groupPosition) {
        return getObjects().get(groupPosition).getValue().size();
    }

    public Object getGroup(int groupPosition) {
        return getObjects().get(groupPosition).getKey();
    }

    public Object getChild(int groupPosition, int childPosition) {
        return getObjects().get(groupPosition).getValue().get(childPosition);
    }

    public long getGroupId(int groupPosition) {
        return ((Integer)groupPosition).longValue();
    }

    public long getChildId(int groupPosition, int childPosition) {
        return ((Integer)childPosition).longValue();
    }

    public boolean hasStableIds() {
        return true;
    }

    public View getGroupView(int groupPosition, boolean isExpanded,
            View convertView, ViewGroup parent) {

        if (convertView != null && convertView.getId() != 
                (isExpanded ? getGroupExpandedView() : getGroupClosedView())) {
//          do nothing, we're good to go, nothing has changed.
        } else {
//          something has changed, update.
            convertView = inflater.inflate(isExpanded ? getGroupExpandedView() :
                    getGroupClosedView(), parent, false);
            convertView.setTag(getObjects().get(groupPosition));
        }

        return convertView;
    }

    public View getChildView(int groupPosition, int childPosition,
            boolean isLastChild, View convertView, ViewGroup parent) {

        if (convertView != null) {
//          do nothing 
        } else {
//          create
            convertView = inflater.inflate(getChildView(), parent, false);
            convertView.setTag(getObjects().get(groupPosition).getValue().get(childPosition));
        }

        return convertView;
    }

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

    public boolean areAllItemsEnabled() {
        return true;
    }

    public boolean isEmpty() {
        return getObjects().size() == 0;
    }

    public void onGroupExpanded(int groupPosition) {

    }

    public void onGroupCollapsed(int groupPosition) {

    }

    public long getCombinedChildId(long groupId, long childId) {
        return groupId * 10000L + childId;
    }

    public long getCombinedGroupId(long groupId) {
        return groupId * 10000L;
    }

    protected DataSetObservable getDataSetObservable() {
        return dataSetObservable;
    }

    protected List<Entry<A, List<B>>> getObjects() {
        return objects;
    }

    protected Context getContext() {
        return context;
    }

    protected Integer getGroupClosedView() {
        return groupClosedView;
    }

    protected Integer getGroupExpandedView() {
        return groupExpandedView;
    }

    protected Integer getChildView() {
        return childView;
    }
}

Any comments or criticisms are welcome.

Naftuli Kay
  • 87,710
  • 93
  • 269
  • 411
  • Rather impressive for whipping it up on the spot. – Ifrit Feb 08 '15 at 22:02
  • 1
    If you extend BaseExpandableListAdapter instead of implementing ExpandableListAdapter then you shouldn't need to implement the methods for combined ids – Phil Jun 23 '17 at 01:45
  • Android SDK _simple and minimal_ built-in view resources for expandable list adapter which can be passed to `AbstractExpandableListAdapter` constructor are `android.R.layout.simple_expandable_list_item_1` for both `groupClosedView` and `groupExpandedView`, and `android.R.layout.simple_list_item_1` for `childView` – Eido95 Jul 10 '17 at 19:56
4

I was fairly surprised I didn't find better documentation on this either. If you find one, please post it here. The best implementation example I found was in ApiDemos. There is an ExpandableListActivity that implements a BaseExpandableListAdapter. The class is called ExpandableList1.java.

You will have to create your own add() method that adds your Group and Child classes to the adapter. I don't think it will be that difficult at first glance. In fact you might just be able to create the references to the class objects. When I implemented mine, my data set was small and didn't change so I only needed to refer to my array.xml file.

user432209
  • 20,007
  • 10
  • 56
  • 75
1
   public class CustomExpandableAdapter extends BaseExpandableListAdapter {
        private Context mContext;
        private List<Group> mData;
        private int mSelectedPosition = -1;

        public CustomExpandableAdapter(Context context, List<Group> data ) {
            mData = data;
            mContext = context;

        }

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

        @Override
        public int getChildrenCount(int groupPosition) {
            return mData.get(groupPosition).children.size();
        }

        @Override
        public Object getGroup(int groupPosition) {
            return mData.get(groupPosition);
        }

        @Override
        public Object getChild(int groupPosition, int childPosition) {
            return mData.get(groupPosition).children.get(childPosition);
        }

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

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

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

        @Override
        public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
            HeaderViewHolder headerViewHolder = null;
            if (convertView == null) {
                convertView = LayoutInflater.from(mContext).inflate(R.layout.faq_header_text_layout, null);
                headerViewHolder = new HeaderViewHolder(convertView);
                convertView.setTag(headerViewHolder);
            }
            headerViewHolder = (HeaderViewHolder) convertView.getTag();

            headerViewHolder.mGroupHeader.setText(mData.get(groupPosition).name);
            return convertView;
        }

        @Override
        public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
            ChildViewHolder childViewHolder = null;
            if (convertView == null) {
                convertView = LayoutInflater.from(mContext).inflate(R.layout.faq_textview_layout, null);
                childViewHolder = new ChildViewHolder(convertView);
                convertView.setTag(childViewHolder);
            }
            childViewHolder = (ChildViewHolder) convertView.getTag();

                        childViewHolder.mChildTitle.setText(mData.get(groupPosition).children.get(childPosition));
            return convertView;
        }

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

        private static class HeaderViewHolder {
            final TextView mGroupHeader;

            private HeaderViewHolder(View group) {
                mGroupHeader = (TextView) group.findViewById(R.id.txv_faq_header_text_layout);
            }
        }

        private static class ChildViewHolder {
            final TextView mChildTitle;

            private ChildViewHolder(View group) {
                mChildTitle = (TextView) group.findViewById(R.id.txv_faq_textview_layout);
            }
        }

        @Override
        public void unregisterDataSetObserver(DataSetObserver observer) {
            if (observer != null) {
                super.unregisterDataSetObserver(observer);
            }
        }

        public void setSelectedPosition(int selectedPosition) {
            mSelectedPosition = selectedPosition;
        }
    }
VikasGoyal
  • 3,308
  • 1
  • 22
  • 42
1

Seeing how old this post and answers are, I thought I would point out there is a very nice 3rd party library that sorta fills in this missing gap. While the posted custom solutions are good, they are still missing some things and are following the cumbersome design of requiring the programmer to generate a data structure of data structures. Sometimes,you just want to organize one List into nice little groups without the hassle of doing it yourself.

It's called the RolodexArrayAdapter and can be easily utilized in creating custom ExpandableListAdapters...without having to worry about all the data management problems and features. It supports methods like add, addAll, remove, removeAll, retainAll, contains, sorting etc. It also supports more advanced features like ChoiceMode, Filtering, and auto expanding groups.

Example:

class MovieAdapter extends RolodexArrayAdapter<Integer, MovieItem> {
    public MovieAdapter(Context activity, List<MovieItem> movies) {
        super(activity, movies);
    }

    @Override
    public Integer createGroupFor(MovieItem childItem) {
        //Lets organize our movies by their release year
        return childItem.year;
    }

    @Override
    public View getChildView(LayoutInflater inflater, int groupPosition, int childPosition,
                             boolean isLastChild, View convertView, ViewGroup parent) {
        if (convertView == null) {
            //Inflate your view
        }
        //Fill view with data
        return convertView;
    }

    @Override
    public View getGroupView(LayoutInflater inflater, int groupPosition, boolean isExpanded,
                             View convertView, ViewGroup parent) {
        if (convertView == null) {
            //Inflate your view
        }
        //Fill view with data
        return convertView;
    }

    @Override
    public boolean hasAutoExpandingGroups() {
        return true;
    }

    @Override
    protected boolean isChildFilteredOut(MovieItem movie, CharSequence constraint) {
        //Lets filter by movie title
        return !movie.title.toLowerCase(Locale.US).contains(
                constraint.toString().toLowerCase(Locale.US));
    }

    @Override
    protected boolean isGroupFilteredOut(Integer year, CharSequence constraint) {
        //Lets filter out everything whose year does not match the numeric values in the constraint.
        return TextUtils.isDigitsOnly(constraint) && !year.toString().contains(constraint);
    }
}
Ifrit
  • 6,791
  • 8
  • 50
  • 79