From f2a1a638db22fbf5a3ed4b1673903bcc1e0f3355 Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 30 Dec 2025 18:27:07 +0100 Subject: [PATCH] DetectDragModifier now detects long-presses The long press initiates an item drag; otherwise the view just scrolls on drag. --- .../schabi/newpipe/ui/DetectDragModifier.kt | 48 +++++++++++++++++-- .../ui/components/menu/LongPressMenuEditor.kt | 12 +++-- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/ui/DetectDragModifier.kt b/app/src/main/java/org/schabi/newpipe/ui/DetectDragModifier.kt index 1a5c6e5dc..ca844d855 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/DetectDragModifier.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/DetectDragModifier.kt @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt index 9f60b356a..5ff418913 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt @@ -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 } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5620f9382..8d0ba0799 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -904,7 +904,7 @@ Popup\nfrom here Play\nfrom here Enabled actions: - Reorder the actions by dragging them around + Reorder the actions by long pressing and then dragging them around Hidden actions: Drag the header or the actions to this section to hide them Header