feat: swipe-back vers l'accueil depuis les scores et l'historique
This commit is contained in:
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.HistoryScreen
|
||||||
import com.livrets.ui.HomeScreen
|
import com.livrets.ui.HomeScreen
|
||||||
import com.livrets.ui.ResultsScreen
|
import com.livrets.ui.ResultsScreen
|
||||||
|
import com.livrets.ui.SwipeBack
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okio.Path.Companion.toPath
|
import okio.Path.Companion.toPath
|
||||||
|
|
||||||
@@ -33,6 +34,10 @@ fun App(
|
|||||||
val history = remember { HistoryRepository("$filesDirPath/history.json".toPath()) }
|
val history = remember { HistoryRepository("$filesDirPath/history.json".toPath()) }
|
||||||
var screen by remember { mutableStateOf<Screen>(Screen.Home) }
|
var screen by remember { mutableStateOf<Screen>(Screen.Home) }
|
||||||
|
|
||||||
|
val homePreview: @Composable () -> Unit = {
|
||||||
|
HomeScreen(onPlay = {}, onHistory = {})
|
||||||
|
}
|
||||||
|
|
||||||
when (val current = screen) {
|
when (val current = screen) {
|
||||||
Screen.Home -> HomeScreen(
|
Screen.Home -> HomeScreen(
|
||||||
onPlay = { config -> screen = Screen.Game(config) },
|
onPlay = { config -> screen = Screen.Game(config) },
|
||||||
@@ -48,15 +53,25 @@ fun App(
|
|||||||
onQuit = { screen = Screen.Home },
|
onQuit = { screen = Screen.Home },
|
||||||
)
|
)
|
||||||
|
|
||||||
is Screen.Results -> ResultsScreen(
|
is Screen.Results -> SwipeBack(
|
||||||
|
onBack = { screen = Screen.Home },
|
||||||
|
background = homePreview,
|
||||||
|
) {
|
||||||
|
ResultsScreen(
|
||||||
result = current.result,
|
result = current.result,
|
||||||
onReplay = { screen = Screen.Game(current.result.config) },
|
onReplay = { screen = Screen.Game(current.result.config) },
|
||||||
onHome = { screen = Screen.Home },
|
onHome = { screen = Screen.Home },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Screen.History -> HistoryScreen(
|
Screen.History -> SwipeBack(
|
||||||
|
onBack = { screen = Screen.Home },
|
||||||
|
background = homePreview,
|
||||||
|
) {
|
||||||
|
HistoryScreen(
|
||||||
history = history,
|
history = history,
|
||||||
onBack = { screen = Screen.Home },
|
onBack = { screen = Screen.Home },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,30 @@
|
|||||||
package com.livrets.ui
|
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 androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
object SwipeBackGesture {
|
object SwipeBackGesture {
|
||||||
val EdgeZone = 24.dp
|
val EdgeZone = 24.dp
|
||||||
@@ -13,3 +37,109 @@ object SwipeBackGesture {
|
|||||||
velocityDpPerSec >= CommitVelocityDpPerSec
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user