Впровадження контекстного меню з довгим натиском для Android-додатків
Довгий натиск — це жест, який користувачі інтуїтивно очікують: утримання на елементі списку відкриває контекстні дії. На практиці впровадження розпадається в двох місцях: неправильне розпізнавання жесту під час швидкого прокручування 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 з анімаціями та мультивибором). Вартість розраховується індивідуально.







