diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/common/EdgeSwipeGuard.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/common/EdgeSwipeGuard.kt new file mode 100644 index 0000000..d138ae9 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/common/EdgeSwipeGuard.kt @@ -0,0 +1,66 @@ +package de.jeanlucmakiola.agendula.ui.common + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.systemGestures +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.unit.dp +import kotlin.math.abs + +/** How far past the system back-gesture zone a horizontal swipe is still ignored. */ +private val EDGE_MARGIN = 24.dp + +/** + * Swallows horizontal swipes that start near the screen's left or right edge, + * before a swipe box below it sees them (#90). A back gesture started just + * inside the system's gesture zone otherwise reaches the app as a row swipe. + * Taps and vertical scrolls are left alone. + */ +@Composable +fun Modifier.ignoringEdgeSwipes(): Modifier { + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + val windowWidth = LocalWindowInfo.current.containerSize.width + val insets = WindowInsets.systemGestures + val margin = with(density) { EDGE_MARGIN.roundToPx() } + val leftEdge = insets.getLeft(density, layoutDirection) + margin + val rightEdge = windowWidth - insets.getRight(density, layoutDirection) - margin + var offsetX by remember { mutableFloatStateOf(0f) } + return this + .onGloballyPositioned { offsetX = it.positionInWindow().x } + .pointerInput(leftEdge, rightEdge) { + val slop = viewConfiguration.touchSlop + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) + val x = offsetX + down.position.x + if (x > leftEdge && x < rightEdge) return@awaitEachGesture + var horizontal = false + while (true) { + val change = awaitPointerEvent(PointerEventPass.Initial).changes + .firstOrNull { it.id == down.id } ?: break + if (!horizontal) { + val delta = change.position - down.position + when { + abs(delta.x) > slop -> horizontal = true + abs(delta.y) > slop -> break + } + } + if (horizontal) change.consume() + if (!change.pressed) break + } + } + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListScreen.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListScreen.kt index 337cd6f..d7bde7c 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/tasklist/TaskListScreen.kt @@ -112,6 +112,7 @@ import de.jeanlucmakiola.agendula.domain.TaskFilter import de.jeanlucmakiola.agendula.domain.TaskSection import de.jeanlucmakiola.agendula.domain.TaskSections import de.jeanlucmakiola.agendula.ui.common.LocalUse24HourFormat +import de.jeanlucmakiola.agendula.ui.common.ignoringEdgeSwipes import de.jeanlucmakiola.agendula.ui.common.priorityAccent import de.jeanlucmakiola.agendula.ui.lists.ListEditorSheet import de.jeanlucmakiola.agendula.ui.lists.listWriteFailureMessage @@ -655,7 +656,7 @@ private fun TaskRow( SwipeToDismissBox( state = dismissState, - modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap), + modifier = modifier.ignoringEdgeSwipes().fillMaxWidth().padding(horizontal = 16.dp).then(gap), // dismissDirection (not targetValue) tracks the live drag, so the colour + // icon reveal as you swipe rather than only once the threshold is crossed. backgroundContent = { SwipeBackground(dismissState.dismissDirection, task.isCompleted) }, @@ -1037,7 +1038,7 @@ private fun SubtaskRow( val gap = if (lastInRun) Modifier else Modifier.padding(bottom = 2.dp) SwipeToDismissBox( state = dismissState, - modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap), + modifier = modifier.ignoringEdgeSwipes().fillMaxWidth().padding(horizontal = 16.dp).then(gap), backgroundContent = { SwipeBackground(dismissState.dismissDirection, task.isCompleted) }, ) { Surface(