feat: swipe-back vers l'accueil depuis les scores et l'historique

This commit is contained in:
herel committed 2026-08-28 16:23:52 +02:00
1 parent bc14056d3e
commit ec31b38d72
2 files changed
+147 -2

No files matched your search

@@ -14,6 +14,7 @@ import com.livrets.ui.GameScreen
import com.livrets.ui.HistoryScreen
import com.livrets.ui.HomeScreen
import com.livrets.ui.ResultsScreen
import com.livrets.ui.SwipeBack
import kotlinx.coroutines.launch
import okio.Path.Companion.toPath
@@ -33,6 +34,10 @@ fun App(
val history = remember { HistoryRepository("$filesDirPath/history.json".toPath()) }
var screen by remember { mutableStateOf<Screen>(Screen.Home) }
val homePreview: @Composable () -> Unit = {
HomeScreen(onPlay = {}, onHistory = {})
}
when (val current = screen) {
Screen.Home -> HomeScreen(
onPlay = { config -> screen = Screen.Game(config) },
@@ -48,15 +53,25 @@ fun App(
onQuit = { screen = Screen.Home },
)
is Screen.Results -> ResultsScreen(
is Screen.Results -> SwipeBack(
onBack = { screen = Screen.Home },
background = homePreview,
) {
ResultsScreen(
result = current.result,
onReplay = { screen = Screen.Game(current.result.config) },
onHome = { screen = Screen.Home },
)
}
Screen.History -> HistoryScreen(
Screen.History -> SwipeBack(
onBack = { screen = Screen.Home },
background = homePreview,
) {
HistoryScreen(
history = history,
onBack = { screen = Screen.Home },
)
}
}
}
@@ -1,6 +1,30 @@
package com.livrets.ui
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlinx.coroutines.launch
object SwipeBackGesture {
val EdgeZone = 24.dp
@@ -13,3 +37,109 @@ object SwipeBackGesture {
velocityDpPerSec >= CommitVelocityDpPerSec
}
}
@Composable
fun SwipeBack(
onBack: () -> Unit,
background: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
val density = LocalDensity.current
val scope = rememberCoroutineScope()
val offset = remember { Animatable(0f) }
val edgeZonePx = with(density) { SwipeBackGesture.EdgeZone.toPx() }
var widthPx by remember { mutableFloatStateOf(0f) }
val showBackground by remember { derivedStateOf { offset.value > 0f } }
Box(
Modifier
.fillMaxSize()
.onSizeChanged { widthPx = it.width.toFloat() },
) {
if (showBackground) {
Box(Modifier.fillMaxSize()) { background() }
}
Box(
Modifier
.fillMaxSize()
.graphicsLayer { translationX = offset.value }
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
if (down.position.x > edgeZonePx) return@awaitEachGesture
val tracker = VelocityTracker()
tracker.addPosition(down.uptimeMillis, down.position)
var dragging = false
var accumulatedX = 0f
var accumulatedY = 0f
var dragged = 0f
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
val change = event.changes.firstOrNull { it.id == down.id } ?: break
if (change.isConsumed && !change.pressed && change.previousPressed) {
if (dragging) {
scope.launch { offset.animateTo(0f, tween(200)) }
}
break
}
if (!dragging && change.isConsumed) break
val delta = change.positionChange()
accumulatedX += delta.x
accumulatedY += delta.y
if (dragging) {
change.consume()
tracker.addPosition(change.uptimeMillis, change.position)
dragged = (dragged + delta.x).coerceAtLeast(0f)
scope.launch { offset.snapTo(dragged) }
} else {
val slop = viewConfiguration.touchSlop
if (abs(accumulatedX) > slop || abs(accumulatedY) > slop) {
if (abs(accumulatedX) > abs(accumulatedY)) {
dragging = true
change.consume()
tracker.resetTracking()
tracker.addPosition(change.uptimeMillis, change.position)
dragged = accumulatedX.coerceAtLeast(0f)
scope.launch { offset.snapTo(dragged) }
} else {
break
}
}
}
if (change.changedToUpIgnoreConsumed()) {
if (dragging) {
val velocityDpPerSec =
tracker.calculateVelocity().x / density.density
val commit = SwipeBackGesture.shouldCommit(
offsetPx = dragged,
containerWidthPx = widthPx,
velocityDpPerSec = velocityDpPerSec,
)
scope.launch {
if (commit) {
offset.snapTo(dragged)
offset.animateTo(widthPx, tween(200))
onBack()
} else {
offset.snapTo(dragged)
offset.animateTo(0f, tween(200))
}
}
}
break
}
}
}
},
) {
content()
}
}
}