3

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

enter image description here

  • second I've opened group 1 and the selection went from the selected items to the children of the group 1

enter image description here

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);
            }
        });
    }
};
CyberGriZzly
  • 379
  • 3
  • 9
  • 22

1 Answers1

1

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.

Ifrit
  • 6,791
  • 8
  • 50
  • 79
  • Thank you for your answer. I did not know about that 3rd party library...that would helped me a lot... – CyberGriZzly Apr 01 '15 at 10:59
  • I managed to do a working example, my own custom code using the position (group id and child id) that comes in the parameters of OnChildClickListener onChildClick method and in the adapters getGroupView and getChildView. I had the same idea about the unique IDs, but I used the position as ID. Your idea is better, I forgot about the IDs of the rows. I edited my answer injecting a snippet of the working code and a little explanations for the people looking for an answer for this issue. – CyberGriZzly Apr 01 '15 at 11:31