fix: resolve gesture conflicts by unifying drag handlers

Replaced competing pointerInput modifiers with a single unified gesture handler that determines drag direction early. This eliminates conflicts between horizontal seeking and vertical brightness/volume gestures, making swipe interactions more reliable and predictable.
This commit is contained in:
2026-02-16 20:29:18 +01:00
parent 733a8b651f
commit 9c83c3629b
2 changed files with 82 additions and 70 deletions

View File

@@ -5,10 +5,10 @@ import kotlin.math.abs
import kotlin.math.pow
internal object HorizontalSeekGestureHelper {
val START_THRESHOLD = 24.dp
private const val COEFFICIENT = 1.3f
const val EXPONENT = 1.8f
private const val MAX_DELTA_MS = 300_000L
val START_THRESHOLD = 12.dp
private const val COEFFICIENT = 3.1f
const val EXPONENT = 1.7f
private const val MAX_DELTA_MS = 12_000_000L
fun deltaMs(rawDelta: Float): Long {
val magnitude = abs(rawDelta)

View File

@@ -1,21 +1,22 @@
package hu.bbara.purefin.player.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.player.helper.HorizontalSeekGestureHelper
import kotlin.math.abs
@Composable
fun PlayerGesturesLayer(
@@ -31,54 +32,56 @@ fun PlayerGesturesLayer(
) {
val density = LocalDensity.current
val horizontalThresholdPx = with(density) { HorizontalSeekGestureHelper.START_THRESHOLD.toPx() }
val directionThresholdPx = with(density) { 20.dp.toPx() } // Threshold to determine drag direction
Box(
modifier = modifier
.fillMaxWidth(0.90f)
.fillMaxHeight(0.70f)
// .background(Color(2f, 2f, 2f, 0.3f))
.pointerInput(Unit) {
detectTapGestures(
onTap = { onTap() },
onDoubleTap = { offset ->
// TODO extract it into an enum
val screenWidth = size.width
val oneThird = screenWidth / 3
val secondThird = oneThird * 2
if (offset.x < oneThird) {
onDoubleTapLeft()
} else if (offset.x >= oneThird && offset.x <= secondThird) {
onDoubleTapCenter()
} else {
onDoubleTapRight()
when {
offset.x < oneThird -> onDoubleTapLeft()
offset.x <= secondThird -> onDoubleTapCenter()
else -> onDoubleTapRight()
}
}
)
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
val horizontalThreshold = size.width / 2
if (change.position.x < horizontalThreshold) {
onVerticalDragLeft(dragAmount.y)
} else {
onVerticalDragRight(dragAmount.y)
}
}
}
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
val startX = down.position.x
var accumulatedDrag = Offset.Zero
var dragDirection: DragDirection? = null
var accumulatedHorizontalDrag = 0f
var isHorizontalDragActive = false
var lastPreviewDelta: Long? = null
detectHorizontalDragGestures(
onDragStart = {
accumulatedHorizontalDrag = 0f
isHorizontalDragActive = false
lastPreviewDelta = null
onHorizontalDragPreview(null)
},
onHorizontalDrag = { change, dragAmount ->
accumulatedHorizontalDrag += dragAmount
if (!isHorizontalDragActive && kotlin.math.abs(accumulatedHorizontalDrag) >= horizontalThresholdPx) {
val dragResult = drag(down.id) { change ->
val delta = change.positionChange()
accumulatedDrag += delta
// Determine direction if not yet determined
if (dragDirection == null && (abs(accumulatedDrag.x) > directionThresholdPx || abs(accumulatedDrag.y) > directionThresholdPx)) {
dragDirection = if (abs(accumulatedDrag.x) > abs(accumulatedDrag.y)) {
DragDirection.HORIZONTAL
} else {
DragDirection.VERTICAL
}
}
// Handle based on determined direction
when (dragDirection) {
DragDirection.HORIZONTAL -> {
accumulatedHorizontalDrag += delta.x
if (!isHorizontalDragActive && abs(accumulatedHorizontalDrag) >= horizontalThresholdPx) {
isHorizontalDragActive = true
}
if (isHorizontalDragActive) {
@@ -89,27 +92,36 @@ fun PlayerGesturesLayer(
onHorizontalDragPreview(deltaMs)
}
}
},
onDragEnd = {
if (isHorizontalDragActive) {
}
DragDirection.VERTICAL -> {
val isLeftSide = startX < size.width / 2
if (isLeftSide) {
onVerticalDragLeft(delta.y)
} else {
onVerticalDragRight(delta.y)
}
}
null -> {
// Direction not determined yet, keep accumulating
}
}
}
// Handle drag end
if (dragDirection == DragDirection.HORIZONTAL && isHorizontalDragActive) {
val deltaMs = HorizontalSeekGestureHelper.deltaMs(accumulatedHorizontalDrag)
if (deltaMs != 0L) {
onHorizontalDrag(deltaMs)
onHorizontalDragPreview(deltaMs)
}
}
accumulatedHorizontalDrag = 0f
isHorizontalDragActive = false
lastPreviewDelta = null
onHorizontalDragPreview(null)
},
onDragCancel = {
accumulatedHorizontalDrag = 0f
isHorizontalDragActive = false
lastPreviewDelta = null
onHorizontalDragPreview(null)
}
}
)
}
)
private enum class DragDirection {
HORIZONTAL,
VERTICAL
}