Нещодавно зіткнулися з кейсом: додаток для 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 в кожному проєкті, що забезпечує сумісність із сучасними стандартами.







