#java #android #popupmenu #recyclerview
Реализую overflow-кнопку для элемента списка, кнопка обрабатывается в холдере адаптера:
class CustomAdapter extends RecyclerView.Adapter {
....
public static class ItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
ImageButton mMenuItem;
CardView mCard;
public ItemHolder(View v) {
super(v);
mCard = (CardView) v.findViewById(R.id.card);
mMenuItem = (ImageButton) v.findViewById(R.id.menu_button);
mMenuItem.setOnClickListener(this);
mCard.setOnClickListener(this);
}
@Override
public void onClick(View v) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
switch (v.getId()) {
case R.id.menu_button:
PopupMenu popupMenu = new PopupMenu(mMenuItem.getContext(),
mMenuItem);
popupMenu.inflate(R.menu.popup_menu);
popupMenu.show();
break;
case R.id.card:
//
break;
}
}
}
}
Если меню раскрыто, то при повороте устройства получаю исключение:
E WindowManager: android.view.WindowLeaked: Activity com.example.android.test.MyActivity
Понятно, что менюшка теряет прошлую активити при пересоздании и, в общем то, это
не приводит к падению приложения и вообще никак не проявляется, если не смотреть logcat,
но хотелось бы решить проблему.
Собственно решение, как таковое, известно - закрывать PopupMenu в методах закрытия
активити, например onPause() - popupmenu.dissmis(). Проблема в том, что это меню создается
в адаптере и ему о жизненном цикле активити ничего не известно. Можно, конечно, уведомить
адаптер, что вызван метод onPause() и закрыть эту менюшку, но получается все довольно
коряво - нужно переносить инициализацию меню в сам адаптер и тп. нежелательные решения.
Посмотрел большое количество примеров реализации, в том числе эталонный iosched,
везде эта проблема просто игнорируется, даже гуглом.
Вопрос такой: как решить и стоит ли вообще обращать внимание на эту проблему или
какие то альтернативные способы реализации меню в айтеме для RecyclerView.
Ответы
Ответ 1
Поскольку вложенный в адаптер класс ItemHolder статический, то для решения проблемы
сделал следующее:
Объявил PopupMenu статическим полем класса - теперь все экземпляры меню будут "связаны",
как будто это один экземпляр:
public static class ItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
ImageButton mMenuItem;
CardView mCard;
public static PopupMenu popupMenu;
...
@Override
public void onClick(View v) {
...
if (position != RecyclerView.NO_POSITION) {
switch (v.getId()) {
case R.id.menu_button:
popupMenu = new PopupMenu(mMenuItem.getContext(), mMenuItem);
...
}
}
}
В onPause() активити, в которую выводится список, обращаюсь напрямую к этому статическому
экземпляру через класс адаптера с предложением закрыться:
@Override
protected void onPause() {
super.onPause();
PopupMenu popupMenu = CustomAdapter.ItemHolder.popupMenu;
if (popupMenu != null) popupMenu.dismiss();
popupMenu = null; // защита от утечек памяти
}
При желании можно написать геттер, чтобы отдавал PopupMenu. В данном случае я не
вижу в этом особой надобности.
PS: По прежнему хотел бы услышать более оптимальное решение.
Ответ 2
Для решения проблемы нам надо:
В нужный момент (при "смерти" активити) закрыть PopupMenu.
А для этого надо получить ссылку на объект PopupMenu.
А оно определено в RecyclerView.ViewHolder.
Значит нам надо получить ссылку на RecyclerView.ViewHolder
И в нём-то и будет искомое PopupMenu.
Добавим поле в холдер для хранения меню, а также геттер:
public static class ItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener
{
ImageButton mMenuItem;
CardView mCard;
//сюда будем ссылку на меню сохранять
PopupMenu popup;
//так будем его из холдера получать
public PopupMenu getPopup()
{
return popup;
}
public ItemHolder(View v) {
super(v);
mCard = (CardView) v.findViewById(R.id.card);
mMenuItem = (ImageButton) v.findViewById(R.id.menu_button);
//устанавливаем тэг одному из элементов разметки
//чтобы потом вызовом View.getTag() получить ссылку на экземпляр холдера
mMenuItem.setTag(this);
mMenuItem.setOnClickListener(this);
mCard.setOnClickListener(this);
}
@Override
public void onClick(View v) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
switch (v.getId()) {
case R.id.menu_button:
//инициализируем поле класса
popup = new PopupMenu(mMenuItem.getContext(), mMenuItem);
popupMenu.inflate(R.menu.popup_menu);
popupMenu.show();
break;
case R.id.card:
//
break;
}
}
}
Все инструменты готовы, теперь можно в методах жизненного цикла Activity или Fragment,
используя лишь ссылку на RecyclerView, находить PopupMenu в элементах списка и вызывать
над ними PopupMenu.dismiss(), что устранит вываливание в логи сообщения об ошибке:
//можно оптимизировать, но для простоты пробежимся по всем элементам
for (int i = 0; i < recyclerView.getLayoutManager().getChildCount(); i++)
{
View recyclerViewElement = recyclerView.getLayoutManager().findViewByPosition(i);
if (recyclerViewElement == null)
{
continue;
}
//находим в элементе View, хранящую ссылку на холдер
View viewThatHoldsReferenceToHolder = recyclerViewElement.findViewById(R.id.menu_button);
if (viewThatHoldsReferenceToHolder == null)
{
continue;
}
//получаем ссылку на холдер
ItemHolder holder = ((ItemHolder) viewThatHoldsReferenceToHolder .getTag());
if (holder != null)
{
//Вот оно - наше меню!
PopupMenu popup = holder.getPopup();
if (popup != null)
{
//Закрываем его и задачу на правку бага
popup.dismiss();
Log.d(LOG, "Красота, а не ругань системы в логах! =)");
}
}
}
В случае же вызывания PopupMenu из адаптера всё гораздо проще - получаем ссылку на
меню из адаптера и... Всё, вопрос решён. Буду считать сей случай доводом в пользу писания
всей логики обработки элемента списка в адаптере, а не в классе холдера.