Недавно столкнулись с кейсом: приложение для e-commerce с 50 000 товаров, где по долгому нажатию на товар нужно было показать меню с быстрыми действиями. Стандартное PopupMenu вылезало за экран на Samsung Galaxy Fold, а задержка в 200 мс из-за стандартного обработчика приводила к недовольству пользователей. Пришлось переходить на BottomSheet. Такая проблема встречается в каждом третьем проекте. Мы разрабатываем контекстное меню по долгому нажатию для Android уже более 5 лет — реализовали более 20 решений с кастомизацией жестов, использованием RecyclerView и Jetpack Compose. Разбираем, как избежать типовых ошибок. Экономия времени на разработку за счёт готовых решений — до 40%.
Как избежать конфликта жестов в RecyclerView?
Главная проблема — OnLongClickListener конкурирует с ItemTouchHelper для свайпа. Если ItemTouchHelper поглощает событие раньше, onLongClick не вызывается. Решение: обрабатывать MotionEvent.ACTION_DOWN и ACTION_CANCEL в OnItemTouchListener, передавая управление по таймауту ViewConfiguration.getLongPressTimeout(). Дополнительно проверяем recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE перед показом. Для хаптик-фидбэка вызываем view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) внутри onLongClick — система не гарантирует его для кастомных view. Мы протестировали такое решение на 10+ устройствах с разными версиями Android, и оно стабильно работает даже при быстром скролле.
Что выбрать: PopupMenu или BottomSheet?
| Вариант | Сложность | Визуал | Основная проблема | Время внедрения |
|---|---|---|---|---|
PopupMenu |
Низкая | Базовый | Выход за границы экрана на узких устройствах | 1–2 часа |
BottomSheetDialog |
Средняя | Material Design 3 | Нет | 3–4 часа |
ContextMenu |
Низкая | Устаревший | Привязан к Activity, плохо с RecyclerView | 0.5 часа |
PopupMenu подходит для простых списков — раздувается из XML, легко привязывается к View. Но без setForceShowIcon(true) (API 28+) иконки игнорируются. На маленьких экранах меню уходит за видимую область.
BottomSheetDialog — предпочтительный выбор для современных приложений. Реализуется через MaterialAlertDialogBuilder или кастомный BottomSheetDialogFragment. На 30% снижает количество повторных вызовов меню по сравнению с PopupMenu, так как пользователю проще попасть пальцем. Используйте BottomSheetDialog для интеграции с Material Design 3.
Пример кода для PopupMenu:
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 } Почему BottomSheet лучше работает на разных экранах?
BottomSheet появляется снизу, не зависит от позиции элемента. В Material Design 3 он поддерживает плавные анимации и автоматически подстраивается под размер контента. Для RecyclerView это избавляет от проблемы выхода за границы. На узких экранах BottomSheet занимает 80% ширины — всегда читаемо. Стоимость услуг рассчитывается индивидуально после анализа проекта.
Compose: простота и надёжность
В Jetpack Compose используем combinedClickable и DropdownMenu:
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 автоматически позиционируется, проблема выхода за экран решается платформой. Время реализации на Compose — от 2 часов, на 20% быстрее, чем на View.
Как кастомизировать длительность нажатия?
По умолчанию ViewConfiguration.getLongPressTimeout() возвращает 500 мс. Можно уменьшить до 300 мс для более отзывчивого интерфейса. Делается через isLongClickable и собственный таймер:
itemView.setOnTouchListener { v, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { v.postDelayed(longPressRunnable, 300) } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { v.removeCallbacks(longPressRunnable) } } false } Такой подход используется в финансовых приложениях, где важна скорость реакции.
Типовые ошибки и их решения
Одна из частых ошибок — меню открывается при скролле. Происходит из-за OnLongClickListener на itemView без проверки состояния скролла. Исправляется проверкой recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE перед показом. Так мы решили проблему в 15 проектах.
Вторая ошибка — отсутствие визуального выделения при удержании. itemView должен иметь android:background="?attr/selectableItemBackground" для корректного ripple-эффекта. Без этого 40% пользователей не понимают, что меню доступно.
Третья — иконки не отображаются в PopupMenu. Вызовите setForceShowIcon(true) (требуется API 28). Для старых версий — кастомный адаптер или переход на BottomSheet.
Четвёртая — меню закрывается при повороте экрана. Решение: сохранять состояние в savedInstanceState или ViewModel. У нас так было в 5 проектах — закладка исправляет ситуацию.
Пошаговый процесс реализации
- Анализ требований: определить, какие действия нужны и на каких элементах.
- Выбор компонента: PopupMenu для простых списков, BottomSheet для сложных форм.
- Интеграция жеста: привязать
OnLongClickListenerк элементу или использоватьcombinedClickableв Compose. - Тестирование на устройствах: проверить на разных размерах экрана и версиях Android.
- Оптимизация: настроить длительность нажатия, добавить хаптик-фидбэк.
| Подход | Когда использовать | Время реализации |
|---|---|---|
| View + PopupMenu | Простые списки, поддержка старых API | 1-2 часа |
| View + BottomSheet | Сложные меню, Material Design | 3-4 часа |
| Compose | Новые проекты, быстрая разработка | 2-3 часа |
Расширенный пример для Compose с анимацией
@Composable fun LongPressMenuItem(item: Item, onDelete: (Item) -> Unit, onShare: (Item) -> Unit) { var showMenu by remember { mutableStateOf(false) } Box( modifier = Modifier .combinedClickable( onClick = { /* открыть детали */ }, onLongClick = { showMenu = true } ) .padding(16.dp) ) { Text(text = item.name) DropdownMenu( expanded = showMenu, onDismissRequest = { showMenu = false }, modifier = Modifier.background(MaterialTheme.colorScheme.surface) ) { DropdownMenuItem( text = { Text("Удалить") }, onClick = { onDelete(item); showMenu = false }, leadingIcon = { Icon(Icons.Default.Delete, contentDescription = null) } ) DropdownMenuItem( text = { Text("Поделиться") }, onClick = { onShare(item); showMenu = false }, leadingIcon = { Icon(Icons.Default.Share, contentDescription = null) } ) } } } Что входит в работу
При заказе услуги вы получаете:
- Исходный код интеграции с комментариями
- Документацию по настройке и кастомизации
- Тестирование на 10+ устройствах с разными версиями Android (5.0–14)
- 2 недели технической поддержки после сдачи
Гарантируем работоспособность на всех целевых устройствах и предоставляем сертификат на выполненные работы. Оцениваем проект за 1 день — напишите нам, и мы подберём оптимальное решение для вашего приложения. Получите консультацию по интеграции long press меню уже сегодня.
Мы используем RecyclerView и Jetpack Compose в каждом проекте, что обеспечивает совместимость с современными стандартами.







