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 open the contextual action menu on a click on a child, I put a tick on the child's view and I change the background of that child
- I open the same contextual action menu on every click on every child
- the closing of the contextual menu I haven't implemented yet (I wanted to see if this works before)
- I than would do the action extracting the children from a Map corresponding to the map that I give to the adapter
- my code works until I click on another group to selected another child
- then, when I open another group the selected children (the tick and the background) moves to the children in that group (I just opened the respective group to select something) and the last selected items got unselected
- I posted screenshots to clarify
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
- second I've opened group 1 and the selection went from the selected items to the children of the group 1
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);
}
});
}
};