Android ExpandableListView, созданный с помощью настраиваемых элементов с множественным выбором, не поддерживает те же выбранные элементы

В моем приложении есть ExpandableListView, в котором мне нужно сделать с контекстным меню действий действие над несколькими дочерними элементами нескольких групп.

В своем исследовании я обнаружил, что множественный выбор в расширяемых представлениях списка невозможен или очень сложно реализовать. Поэтому я решил реализовать свое собственное решение следующим образом (я разместил код ниже для пояснения, это черновик кода, он не является окончательным, я только что реализовал/жестко закодировал некоторые вещи, чтобы увидеть, если это работает или нет):

  • Я открываю контекстное меню действий по щелчку на дочернем элементе, ставлю галочку в представлении дочернего элемента и меняю фон этого дочернего элемента.
  • Я открываю одно и то же контекстное меню действий при каждом щелчке по каждому дочернему элементу.
  • закрытие контекстного меню, которое я еще не реализовал (хотел проверить, работает ли это раньше)
  • Я бы сделал действие, извлекая дочерние элементы из карты, соответствующей карте, которую я даю адаптеру.
  • мой код работает, пока я не нажму на другую группу, чтобы выбрать другого ребенка
  • затем, когда я открываю другую группу, выбранные дочерние элементы (галочка и фон) перемещаются к дочерним элементам в этой группе (я только что открыл соответствующую группу, чтобы что-то выбрать), а последние выбранные элементы не были выбраны
  • Я выложил скриншоты для ясности
  • Я не заметил какой-либо модели поведения

    Я не знаю, почему это происходит.

адаптер:

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;
}

}

фрагмент из расширяемого представления списка:

    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;
    }
};

скрины: - сначала выбрал из группы 2, пункты 1, 2

введите здесь описание изображения

  • во-вторых, я открыл группу 1, и выбор пошел от выбранных элементов к детям группы 1.

введите здесь описание изображения

И что хуже всего, это поведение непоследовательно, иногда оно работает так, как задумано, но в большинстве случаев это не так, может быть, это происходит чисто случайно.

ИЗМЕНИТЬ

Как я уже упоминал в комментарии ниже, я публикую черновик своего рабочего пользовательского кода для людей, которые ищут ответ в этом выпуске. Это не окончательный код, но он работает и дает представление о решении.

Предложение Джея Сойера о сторонней библиотеке выглядит довольно хорошо и проверено в производстве.

Когда я нажимаю на ребенка, вызывается setOnChildClickListener onClick. Там выполняется первая проверка позиции (она добавлена ​​или нет в пользовательской структуре данных), и я соответственно выбираю/отменяю выбор дочернего элемента.

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;
        }
    });

Впоследствии, когда я нажимаю на группу, чтобы расширить/свернуть группу, вызываются методы адаптеров getGroupView и getChildView. И в тех методах я снова делаю подобную проверку. Я сохраняю позицию группы, дочернюю позицию и вид в списке (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;
}

И мне удалось поймать кнопку галочки контекстного меню на прослушивателе кликов, чтобы вызвать метод unSelectAllChildren

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);
            }
        });
    }
};

person CyberGriZzly    schedule 26.03.2015    source источник


Ответы (1)


Проблема связана со стабильными идентификаторами. К сожалению, у Android мало документации не только о том, зачем они вам нужны, но и о том, как их использовать. В вашем случае вы правильно возвращаете true для hasStableIds(), однако на самом деле вы не возвращаете стабильные идентификаторы.

Вы переопределяете getGroupId(), но также необходимо переопределить getChildId(). Кроме того, ваша реализация getGroupId() по-прежнему неверна. Идентификатор должен быть не только уникальным среди всех позиций... он также должен быть стабильным. Это означает, что идентификатор группы Dell должен быть одинаковым, независимо от того, в какой позиции он хранится. Не подходит только возврат самой позиции для идентификатора.

Вы правы в том, что вы должны вручную реализовать режим выбора, если хотите его использовать, и да, это болезненный процесс. Хотя вы, вероятно, уже потратили много времени на развертывание собственного решения, знайте, что существует сторонняя библиотека который уже предоставляет решение для вас. Он используется и тестируется в производственном коде и очень надежен. Там есть много примеров кода и демонстрационное приложение, на которое вы также можете ссылаться.

Библиотека специально содержит PatchedExpandabeListAdapter, который исправляет некоторые проблемы в работе с помощью ExpandableListAdapter...включить режим выбора. Вы бы использовали этого парня, чтобы написать свой собственный адаптер (в отношении управления данными). Если вы даже не хотите беспокоиться об этом, он также предоставляет Rolodex Адаптеры, которые в основном обрабатывают все, кроме создания представлений.

person Jay Soyer    schedule 31.03.2015
comment
Спасибо за ваш ответ. Я не знал об этой сторонней библиотеке... она бы мне очень помогла... - person CyberGriZzly; 01.04.2015
comment
Мне удалось сделать рабочий пример, мой собственный код, используя позицию (идентификатор группы и идентификатор ребенка), которая входит в параметры метода OnChildClickListener onChildClick и в адаптерах getGroupView и getChildView. У меня было такое же представление об уникальных идентификаторах, но я использовал позицию в качестве идентификатора. Ваша идея лучше, я забыл про идентификаторы строк. Я отредактировал свой ответ, внедрив фрагмент рабочего кода и небольшие пояснения для людей, которые ищут ответ на этот вопрос. - person CyberGriZzly; 01.04.2015