In my application there is a ExpandableListView in which I need to do with contextual action menu an action on several children of several groups.
In my research I've found that multi-choice on expandable list views is not possible or really hard to implement. So I've decided to implement my custom solution as follows (I've posted the code bellow for clarification, it is a draft of the code, it is not final, I've just implemented / hard coded some things to see if it works or not):
I haven't observed any pattern of the behaviour
I don't know why this is happening.
the adapter:
public class CoordinateExpandableListAdapter extends BaseExpandableListAdapter {
private Context context;
private Map<String, List<String>> coordinateList;
private List<String> groupList;
public CoordinateExpandableListAdapter(Context context, List<String> groupList, Map<String, List<String>> coordinateList) {
this.context = context;
this.groupList = groupList;
this.coordinateList = coordinateList;
}
@Override
public Object getChild(int groupPosition, int childPosition) {
return coordinateList.get(groupList.get(groupPosition)).get(childPosition);
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
class ChildRowHolder {
TextView childRowTitle;
public ChildRowHolder(View view) {
childRowTitle = (TextView) view.findViewById(R.id.child_item_expandable_list_view_title);
}
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
View childRow = convertView;
ChildRowHolder childRowHolder = null;
String childRowTitle = (String) getChild(groupPosition, childPosition);
if (childRow == null) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
childRow = inflater.inflate(R.layout.child_expandable_list_view_item, null);
childRowHolder = new ChildRowHolder(childRow);
childRow.setTag(childRowHolder);
} else {
childRowHolder = (ChildRowHolder) childRow.getTag();
}
childRowHolder.childRowTitle.setText(childRowTitle);
return childRow;
}
@Override
public int getChildrenCount(int groupPosition) {
return coordinateList.get(groupList.get(groupPosition)).size();
}
@Override
public Object getGroup(int groupPosition) {
return groupList.get(groupPosition);
}
@Override
public int getGroupCount() {
return groupList.size();
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
class GroupRowHolder {
TextView groupRowTitle;
public GroupRowHolder(View view) {
groupRowTitle = (TextView) view.findViewById(R.id.group_item_expandable_list_view);
}
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
View groupRow = convertView;
GroupRowHolder groupRowHolder = null;
String coodinateCategory = (String) getGroup(groupPosition);
if (groupRow == null) {
LayoutInflater inflator = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
groupRow = inflator.inflate(R.layout.group_expandable_list_view_item, null);
groupRowHolder = new GroupRowHolder(groupRow);
groupRow.setTag(groupRowHolder);
} else {
groupRowHolder = (GroupRowHolder) groupRow.getTag();
}
groupRowHolder.groupRowTitle.setText(coodinateCategory);
groupRowHolder.groupRowTitle.setTypeface(null, Typeface.BOLD);
return groupRow;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
}
the snippet from the expandable list view:
expandableListView = (ExpandableListView) getActivity().findViewById(R.id.coordinate_expandable_list_view);
CoordinateExpandableListAdapter expandableListAdapter = new CoordinateExpandableListAdapter(getActivity(), groupList, coordinateList);
expandableListView.setAdapter(expandableListAdapter);
registerForContextMenu(expandableListView);
expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View childView, int groupPosition, int childPosition, long id) {
// Start the CAB using the ActionMode.Callback defined above
actionMode = getActivity().startActionMode(actionModeCallback);
ArrayList<Integer> positions = new ArrayList<Integer>();
positions.add(groupPosition);
positions.add(childPosition);
Integer key = -1;
if ((key = isPositionsAdded(positions)) == null) {
selectedMap.put(selectedMapKey++, positions);
ImageView tick = (ImageView) childView.findViewById(R.id.child_item_expandable_list_view_selected);
tick.setVisibility(View.VISIBLE);
childView.setBackgroundColor(getResources().getColor(R.color.backgroung_expandable_list_view_child));
} else {
selectedMap.remove(key);
ImageView tick = (ImageView) childView.findViewById(R.id.child_item_expandable_list_view_selected);
tick.setVisibility(View.GONE);
childView.setBackgroundColor(getResources().getColor(R.color.background_color));
}
return true;
}
});
expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
@Override
public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
// TODO Auto-generated method stub
return false;
}
});
}
private Integer isPositionsAdded(ArrayList<Integer> positions) {
if (selectedMap.containsValue(positions)) {
for (Map.Entry<Integer, ArrayList<Integer>> entry : selectedMap.entrySet()) {
if (entry.getValue().equals(positions)) {
return entry.getKey();
}
}
}
return null;
}
private ActionMode.Callback actionModeCallback = new ActionMode.Callback() {// Called when the action mode is created; startActionMode() was called
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.coordinate_list_context_menu, menu);
return true;
}
// Called each time the action mode is shown. Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; // Return false if nothing is done
}
// Called when the user selects a contextual menu item
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.delete:
deleteCurrentItems();
mode.finish(); // Action picked, so close the CAB
return true;
default:
return false;
}
}
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
}
};
screenshots: - first I've selected from the group 2, items 1, 2


And what is worst, that this behaviour isn't consistent, sometimes it works as intended, but the most of the times it doesn't, maybe the times it does it is purely coincidental
EDIT
As I mentioned in the comment bellow I post my working custom code draft for the people looking for answer in this issue. It is not final code but it is working and gives an idea about the solution.
The suggestion of Jay Soyer about the 3rd party library looks pretty good and tested in production.
When I click on a child the setOnChildClickListener onClick is called. There a do a first verification of the position (it is added or not in a custom data structure) and I select / deselect the child accordingly.
expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View childView, int groupPosition, int childPosition, long id) {
// check / not check a child
ArrayList<Object> position = new ArrayList<Object>();
position.add(groupPosition);
position.add(childPosition);
position.add(childView);
if (expandableListAdapter.isPositionAleadyAdded(position) == null) {
expandableListAdapter.selectChild(position);
} else {
expandableListAdapter.deSelectChild(position);
}
// Start the CAB using the ActionMode.Callback
// defined above
actionMode = getActivity().startActionMode(actionModeCallback);
// set title of contextual action mode
setContextualMenuTitle((ActionMode) actionMode);
return true;
}
});
Afterwards when I click on a group to expand / contract the group the adapters getGroupView and getChildView method are called. And in those method I do a similar verification again. I save the group position, the child position and the view in a list (ArrayList).
private List<ArrayList<Object>> selectedChildren = new ArrayList<ArrayList<Object>>();
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
View childView = convertView;
ChildRowHolder childRowHolder = null;
String childRowTitle = (String) getChild(groupPosition, childPosition);
if (childView == null) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
childView = inflater.inflate(R.layout.child_expandable_list_view_item, null);
childRowHolder = new ChildRowHolder(childView);
childView.setTag(childRowHolder);
} else {
childRowHolder = (ChildRowHolder) childView.getTag();
}
childRowHolder.childRowTitle.setText(childRowTitle);
// select / deselect child
ArrayList<Object> position = new ArrayList<Object>();
position.add(groupPosition);
position.add(childPosition);
position.add(childView);
if (isPositionAleadyAdded(position) != null) {
selectChild(position);
} else {
deSelectChild(position);
}
return childView;
}
/**
* This method return the key in the selected child map if it exists already. Null otherwise.
*
* @param position the group and child position
* @return the key of the element of the map if it exists already, null otherwise
*/
public ArrayList<Object> isPositionAleadyAdded(ArrayList<Object> position) {
for (ArrayList<Object> entry : selectedChildren) {
if (entry.get(0) == position.get(0)
&& entry.get(1) == position.get(1)) {
return position;
}
}
return null;
}
/**
* This method does the selection of a child.
*
* @param childView view of child
* @param position position of child
*/
public void selectChild(ArrayList<Object> position) {
if (isPositionAleadyAdded(position) == null) {
selectedChildren.add(position);
}
LinearLayout childView = (LinearLayout) position.get(2);
ImageView tick = (ImageView) childView.findViewById(R.id.child_item_expandable_list_view_selected);
tick.setVisibility(View.VISIBLE);
childView.setBackgroundColor(context.getResources().getColor(R.color.backgroung_expandable_list_view_child));
}
/**
* This method un selects a child.
*
* @param childView view of child
* @param position position of child
*/
public void deSelectChild(ArrayList<Object> position) {
ArrayList<Object> pos = null;
int elemNo = -1;
if ((pos = isPositionAleadyAdded(position)) != null) {
elemNo = getNoElemInSelectedChildred(pos);
selectedChildren.remove(elemNo);
}
LinearLayout childView = (LinearLayout) position.get(2);
ImageView tick = (ImageView) childView.findViewById(R.id.child_item_expandable_list_view_selected);
tick.setVisibility(View.GONE);
childView.setBackgroundColor(context.getResources().getColor(R.color.background_color));
}
private int getNoElemInSelectedChildred(ArrayList<Object> position) {
int index = 0;
for (ArrayList<Object> entry : selectedChildren) {
if (entry.get(0) == position.get(0) && entry.get(1) == position.get(1)) {
return index;
} else {
index++;
}
}
return -1;
}
/*
* This method clears the map of all children.
*/
public void unSelectAllChildren() {
for (ArrayList<Object> entry : selectedChildren) {
LinearLayout childView = (LinearLayout) entry.get(2);
ImageView tick = (ImageView) childView.findViewById(R.id.child_item_expandable_list_view_selected);
tick.setVisibility(View.GONE);
childView.setBackgroundColor(context.getResources().getColor(R.color.background_color));
}
selectedChildren.clear();
}
public List<ArrayList<Object>> getSelectedChildren() {
return selectedChildren;
}
And I managed to catch the context menus tick button on click listener to call the unSelectAllChildren method
private ActionMode.Callback actionModeCallback = new ActionMode.Callback() {
...
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(final ActionMode mode) {
int doneButtonId = Resources.getSystem().getIdentifier("action_mode_close_button", "id", "android");
LinearLayout layout = (LinearLayout) getActivity().findViewById(doneButtonId);
ImageView doneview = (ImageView) layout.getChildAt(0);
doneview.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
expandableListAdapter.unSelectAllChildren();
setContextualMenuTitle(mode);
}
});
}
};
The issue revolves around stable IDs. Unfortunately there's little documentation from Android on not only why you need them but how to use them. In your case, you correctly return true for hasStableIds() however you fail to actually return stable Ids.
You override getGroupId() but also need to override getChildId(). Also, your getGroupId() implementation is still incorrect. An Id must no only be unique among all the positions...it must also be stable. Meaning, the Id for group Dell must be the same no matter what position it's stored at. Only returning the position itself for an Id does not qualify.
You are correct that you must manually implement choice mode if you wish to use it, and yes it's a painful process. While you probably already put a lot of time into rolling your own solution, do know there's a 3rd party library which already provides the solution for you. It's used and tested in production code and is very reliable. There's plenty of example code and a demo app for you to reference as well.
The library specifically contains a PatchedExpandabeListAdapter which patches some of the issues in working with the ExpandableListAdapter...to include choice mode. You'd use this guy to write your own custom adapter (in regards to data management). If you don't even want to worry about that, it also provides Rolodex Adapters which basically handles everything but view generation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With