Реализация Long Press Menu для Android-приложения
Long press — это жест, который пользователи ожидают интуитивно: долгое нажатие на элемент списка открывает контекстные действия. На практике реализация разваливается в двух местах: некорректное распознавание жеста при быстром скролле RecyclerView и визуально невнятное отображение меню.
Варианты реализации
PopupMenu — самый простой путь для списков с фиксированными действиями. Прикрепляется к View через якорь, раздувается из XML-ресурса:
itemView.setOnLongClickListener { view ->
val popup = PopupMenu(view.context, view)
popup.menuInflater.inflate(R.menu.context_item_menu, popup.menu)
popup.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.action_delete -> { onDelete(item); true }
R.id.action_share -> { onShare(item); true }
else -> false
}
}
popup.show()
true
}
Проблема: PopupMenu появляется в углу View без анимации позиции. На маленьких экранах или в нижней части списка меню уходит за границы видимой области. Начиная с API 28 setForceShowIcon(true) позволяет показывать иконки в PopupMenu — без этого флага иконки из menu.xml просто игнорируются.
ContextMenu через registerForContextMenu() — устаревший подход, привязан к Activity и плохо работает с RecyclerView.
BottomSheetDialog — предпочтительный выбор для современных приложений с Material Design 3. Визуально чище, не зависит от позиции элемента на экране, легко расширяется. Реализуется через MaterialAlertDialogBuilder или кастомный BottomSheetDialogFragment.
Проблема скролла и жеста
В RecyclerView OnLongClickListener конкурирует с ItemTouchHelper, если тот используется для свайпа. Если ItemTouchHelper поглощает событие раньше — onLongClick не вызывается. Решение: явно обрабатывать MotionEvent.ACTION_DOWN / ACTION_CANCEL в OnItemTouchListener и передавать управление по таймауту ViewConfiguration.getLongPressTimeout().
Другой случай: хаптик-фидбэк при долгом нажатии. view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) — вызывается вручную внутри onLongClick, потому что система не гарантирует его автоматически для кастомных view.
Compose
В Jetpack Compose — combinedClickable:
Box(
modifier = Modifier.combinedClickable(
onClick = { onClick(item) },
onLongClick = { showMenu = true }
)
) {
// контент элемента
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(text = { Text("Удалить") }, onClick = { onDelete(item); showMenu = false })
DropdownMenuItem(text = { Text("Поделиться") }, onClick = { onShare(item); showMenu = false })
}
}
DropdownMenu позиционируется автоматически относительно родительского Box — проблема выхода за границы экрана решается платформой.
Типичные ошибки
Меню открывается при скролле. Причина — OnLongClickListener на itemView без проверки состояния скролла. Фикс: recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE перед показом меню.
Нет визуального выделения при удержании. itemView должен иметь android:background="?attr/selectableItemBackground" для корректного ripple-эффекта при долгом нажатии.
Сложность реализации — от нескольких часов (простой PopupMenu) до 1-2 дней (кастомный BottomSheet с анимациями и мультиселектом). Стоимость рассчитывается индивидуально.







