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 import kotlin.math.pow
internal object HorizontalSeekGestureHelper { internal object HorizontalSeekGestureHelper {
val START_THRESHOLD = 24.dp val START_THRESHOLD = 12.dp
private const val COEFFICIENT = 1.3f private const val COEFFICIENT = 3.1f
const val EXPONENT = 1.8f const val EXPONENT = 1.7f
private const val MAX_DELTA_MS = 300_000L private const val MAX_DELTA_MS = 12_000_000L
fun deltaMs(rawDelta: Float): Long { fun deltaMs(rawDelta: Float): Long {
val magnitude = abs(rawDelta) val magnitude = abs(rawDelta)

View File

@@ -1,21 +1,22 @@
package hu.bbara.purefin.player.ui.components package hu.bbara.purefin.player.ui.components
import androidx.compose.foundation.background import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.border import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.player.helper.HorizontalSeekGestureHelper import hu.bbara.purefin.player.helper.HorizontalSeekGestureHelper
import kotlin.math.abs
@Composable @Composable
fun PlayerGesturesLayer( fun PlayerGesturesLayer(
@@ -31,85 +32,96 @@ fun PlayerGesturesLayer(
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val horizontalThresholdPx = with(density) { HorizontalSeekGestureHelper.START_THRESHOLD.toPx() } val horizontalThresholdPx = with(density) { HorizontalSeekGestureHelper.START_THRESHOLD.toPx() }
val directionThresholdPx = with(density) { 20.dp.toPx() } // Threshold to determine drag direction
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth(0.90f) .fillMaxWidth(0.90f)
.fillMaxHeight(0.70f) .fillMaxHeight(0.70f)
// .background(Color(2f, 2f, 2f, 0.3f))
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onTap = { onTap() }, onTap = { onTap() },
onDoubleTap = { offset -> onDoubleTap = { offset ->
// TODO extract it into an enum
val screenWidth = size.width val screenWidth = size.width
val oneThird = screenWidth / 3 val oneThird = screenWidth / 3
val secondThird = oneThird * 2 val secondThird = oneThird * 2
if (offset.x < oneThird) { when {
onDoubleTapLeft() offset.x < oneThird -> onDoubleTapLeft()
} else if (offset.x >= oneThird && offset.x <= secondThird) { offset.x <= secondThird -> onDoubleTapCenter()
onDoubleTapCenter() else -> onDoubleTapRight()
} else {
onDoubleTapRight()
} }
} }
) )
} }
.pointerInput(Unit) { .pointerInput(Unit) {
detectDragGestures { change, dragAmount -> awaitEachGesture {
val horizontalThreshold = size.width / 2 val down = awaitFirstDown(requireUnconsumed = false)
if (change.position.x < horizontalThreshold) { val startX = down.position.x
onVerticalDragLeft(dragAmount.y)
} else { var accumulatedDrag = Offset.Zero
onVerticalDragRight(dragAmount.y) var dragDirection: DragDirection? = null
var accumulatedHorizontalDrag = 0f
var isHorizontalDragActive = false
var lastPreviewDelta: Long? = null
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) {
change.consume()
val deltaMs = HorizontalSeekGestureHelper.deltaMs(accumulatedHorizontalDrag)
if (deltaMs != 0L && deltaMs != lastPreviewDelta) {
lastPreviewDelta = deltaMs
onHorizontalDragPreview(deltaMs)
}
}
}
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)
}
}
onHorizontalDragPreview(null)
} }
} }
.pointerInput(Unit) {
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) {
isHorizontalDragActive = true
}
if (isHorizontalDragActive) {
change.consume()
val deltaMs = HorizontalSeekGestureHelper.deltaMs(accumulatedHorizontalDrag)
if (deltaMs != 0L && deltaMs != lastPreviewDelta) {
lastPreviewDelta = deltaMs
onHorizontalDragPreview(deltaMs)
}
}
},
onDragEnd = {
if (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
}