DetectDragModifier now detects long-presses

The long press initiates an item drag; otherwise the view just scrolls on drag.
This commit is contained in:
Stypox 2025-12-30 18:27:07 +01:00
parent 032a853072
commit f2a1a638db
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
3 changed files with 53 additions and 9 deletions

View File

@ -1,37 +1,75 @@
package org.schabi.newpipe.ui
import android.view.MotionEvent
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.isOutOfBounds
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
/**
* Detects a drag gesture **without** trying to filter out any misclicks. This is useful in menus
* where items are dragged around, where the usual misclick guardrails would cause unexpected lags
* or strange behaviors when dragging stuff around quickly. For other use cases, use
* [androidx.compose.foundation.gestures.detectDragGestures] or
* or strange behaviors when dragging stuff around quickly. Also detects whether a drag gesture
* began with a long press or not, which can be useful to decide whether an item should be dragged
* around (in case of long-press) or the view should be scrolled (otherwise). For other use cases,
* use [androidx.compose.foundation.gestures.detectDragGestures] or
* [androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress].
*
* @param beginDragGesture called when the user first touches the screen (down event) with the
* pointer position.
* pointer position and whether a long press was detected.
* @param handleDragGestureChange called with the current pointer position and the difference from
* the last position, every time the user moves the finger after [beginDragGesture] has been called.
* @param endDragGesture called when the drag gesture finishes, after [beginDragGesture] has been
* called.
*/
fun Modifier.detectDragGestures(
beginDragGesture: (position: IntOffset) -> Unit,
beginDragGesture: (position: IntOffset, wasLongPressed: Boolean) -> Unit,
handleDragGestureChange: (position: IntOffset, positionChange: Offset) -> Unit,
endDragGesture: () -> Unit
): Modifier {
return this.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
val wasLongPressed = try {
// code in this branch was taken from AwaitPointerEventScope.waitForLongPress(),
// which unfortunately is private
withTimeout(viewConfiguration.longPressTimeoutMillis) {
while (true) {
val event = awaitPointerEvent()
if (event.changes.fastAll { it.changedToUp() }) {
// All pointers are up
break
}
if (event.classification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
return@withTimeout true
}
if (
event.changes.fastAny {
it.isConsumed || it.isOutOfBounds(IntSize(0, 0), extendedTouchPadding)
}
) {
break
}
}
return@withTimeout false
}
} catch (_: PointerEventTimeoutCancellationException) {
true
}
val pointerId = down.id
beginDragGesture(down.position.toIntOffset())
beginDragGesture(down.position.toIntOffset(), wasLongPressed)
while (true) {
val change = awaitPointerEvent().changes.find { it.id == pointerId }
if (change == null || !change.pressed) {

View File

@ -94,7 +94,7 @@ import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
const val TAG = "LongPressMenuEditor"
internal const val TAG = "LongPressMenuEditor"
// TODO padding doesn't seem to work as expected when the list becomes scrollable?
@Composable
@ -166,7 +166,12 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
}
// this beginDragGesture() overload is only called when moving the finger (not on DPAD's Enter)
fun beginDragGesture(pos: IntOffset) {
fun beginDragGesture(pos: IntOffset, wasLongPressed: Boolean) {
if (!wasLongPressed) {
// items can be dragged around only if they are long-pressed;
// use the drag as scroll otherwise
return
}
val rawItem = findItemForOffsetOrClosestInRow(pos) ?: return
beginDragGesture(pos, rawItem)
autoScrollSpeed = 0f
@ -229,7 +234,8 @@ fun LongPressMenuEditor(modifier: Modifier = Modifier) {
fun handleDragGestureChange(pos: IntOffset, posChangeForScrolling: Offset) {
val dragItem = activeDragItem
if (dragItem == null) {
// when the user clicks outside of any draggable item, let the list be scrolled
// when the user clicks outside of any draggable item, or if the user did not long-press
// on an item to begin with, let the list be scrolled
gridState.dispatchRawDelta(-posChangeForScrolling.y)
return
}

View File

@ -904,7 +904,7 @@
<string name="popup_from_here">Popup\nfrom here</string>
<string name="play_from_here">Play\nfrom here</string>
<string name="long_press_menu_enabled_actions">Enabled actions:</string>
<string name="long_press_menu_enabled_actions_description">Reorder the actions by dragging them around</string>
<string name="long_press_menu_enabled_actions_description">Reorder the actions by long pressing and then dragging them around</string>
<string name="long_press_menu_hidden_actions">Hidden actions:</string>
<string name="long_press_menu_hidden_actions_description">Drag the header or the actions to this section to hide them</string>
<string name="header">Header</string>