refactor: densité vivante, constante settle, tests de limite swipe-back

This commit is contained in:
herel committed 2026-08-28 16:56:00 +02:00
1 parent d480c1a0cd
commit 1e6b1e488e
3 files changed
+40 -19

No files matched your search

+11 -11
View File
@@ -32,7 +32,7 @@ Spec : `docs/superpowers/specs/2026-08-28-swipe-back-design.md`
- Consumes: rien (fonction pure) - Consumes: rien (fonction pure)
- Produces: `SwipeBackGesture.EdgeZone: Dp`, `SwipeBackGesture.CommitRatio: Float`, `SwipeBackGesture.CommitVelocityDpPerSec: Float`, `SwipeBackGesture.shouldCommit(offsetPx: Float, containerWidthPx: Float, velocityDpPerSec: Float): Boolean` — consommés par le Task 2 - Produces: `SwipeBackGesture.EdgeZone: Dp`, `SwipeBackGesture.CommitRatio: Float`, `SwipeBackGesture.CommitVelocityDpPerSec: Float`, `SwipeBackGesture.shouldCommit(offsetPx: Float, containerWidthPx: Float, velocityDpPerSec: Float): Boolean` — consommés par le Task 2
- [ ] **Step 1: Écrire le test qui échoue** - [x] **Step 1: Écrire le test qui échoue**
Créer `sharedUI/src/commonTest/kotlin/com/livrets/ui/SwipeBackGestureTest.kt` : Créer `sharedUI/src/commonTest/kotlin/com/livrets/ui/SwipeBackGestureTest.kt` :
@@ -91,12 +91,12 @@ class SwipeBackGestureTest {
} }
``` ```
- [ ] **Step 2: Vérifier que le test échoue** - [x] **Step 2: Vérifier que le test échoue**
Run: `./gradlew :sharedUI:jvmTest --tests "com.livrets.ui.SwipeBackGestureTest"` Run: `./gradlew :sharedUI:jvmTest --tests "com.livrets.ui.SwipeBackGestureTest"`
Expected: FAIL — compilation error `unresolved reference: SwipeBackGesture` Expected: FAIL — compilation error `unresolved reference: SwipeBackGesture`
- [ ] **Step 3: Implémenter la logique minimale** - [x] **Step 3: Implémenter la logique minimale**
Créer `sharedUI/src/commonMain/kotlin/com/livrets/ui/SwipeBack.kt` (le composable viendra au Task 2, ce fichier ne contient que l'objet pour l'instant) : Créer `sharedUI/src/commonMain/kotlin/com/livrets/ui/SwipeBack.kt` (le composable viendra au Task 2, ce fichier ne contient que l'objet pour l'instant) :
@@ -118,12 +118,12 @@ object SwipeBackGesture {
} }
``` ```
- [ ] **Step 4: Vérifier que le test passe** - [x] **Step 4: Vérifier que le test passe**
Run: `./gradlew :sharedUI:jvmTest --tests "com.livrets.ui.SwipeBackGestureTest"` Run: `./gradlew :sharedUI:jvmTest --tests "com.livrets.ui.SwipeBackGestureTest"`
Expected: PASS — 4 tests Expected: PASS — 4 tests
- [ ] **Step 5: Commit** - [x] **Step 5: Commit**
```bash ```bash
git add sharedUI/src/commonMain/kotlin/com/livrets/ui/SwipeBack.kt sharedUI/src/commonTest/kotlin/com/livrets/ui/SwipeBackGestureTest.kt git add sharedUI/src/commonMain/kotlin/com/livrets/ui/SwipeBack.kt sharedUI/src/commonTest/kotlin/com/livrets/ui/SwipeBackGestureTest.kt
@@ -142,7 +142,7 @@ git commit -m "feat: logique de décision du swipe-back"
- Consumes: `SwipeBackGesture` (Task 1) - Consumes: `SwipeBackGesture` (Task 1)
- Produces: `@Composable fun SwipeBack(onBack: () -> Unit, background: @Composable () -> Unit, content: @Composable () -> Unit)` — utilisé dans `App.kt` - Produces: `@Composable fun SwipeBack(onBack: () -> Unit, background: @Composable () -> Unit, content: @Composable () -> Unit)` — utilisé dans `App.kt`
- [ ] **Step 1: Compléter `SwipeBack.kt` avec le composable** - [x] **Step 1: Compléter `SwipeBack.kt` avec le composable**
Ajouter sous l'objet `SwipeBackGesture` (le fichier final complet) : Ajouter sous l'objet `SwipeBackGesture` (le fichier final complet) :
@@ -302,12 +302,12 @@ Points de conception (pour le relecteur) :
- `showBackground` via `derivedStateOf` : recomposition uniquement au passage 0 ↔ >0, le `translationX` animé ne recompose pas le layout - `showBackground` via `derivedStateOf` : recomposition uniquement au passage 0 ↔ >0, le `translationX` animé ne recompose pas le layout
- Le fond n'est composé que pendant un drag (aucun coût hors geste) - Le fond n'est composé que pendant un drag (aucun coût hors geste)
- [ ] **Step 2: Vérifier la compilation et les tests existants** - [x] **Step 2: Vérifier la compilation et les tests existants**
Run: `./gradlew :sharedUI:jvmTest` Run: `./gradlew :sharedUI:jvmTest`
Expected: PASS — les 20 tests existants + 4 nouveaux (24 tests) Expected: PASS — les 20 tests existants + 4 nouveaux (24 tests)
- [ ] **Step 3: Brancher Results et History dans `App.kt`** - [x] **Step 3: Brancher Results et History dans `App.kt`**
Dans `sharedUI/src/commonMain/kotlin/com/livrets/App.kt`, remplacer (lignes 51-60) : Dans `sharedUI/src/commonMain/kotlin/com/livrets/App.kt`, remplacer (lignes 51-60) :
@@ -367,12 +367,12 @@ import com.livrets.ui.ResultsScreen
import com.livrets.ui.SwipeBack import com.livrets.ui.SwipeBack
``` ```
- [ ] **Step 4: Recompiler et relancer les tests** - [x] **Step 4: Recompiler et relancer les tests**
Run: `./gradlew :sharedUI:jvmTest` Run: `./gradlew :sharedUI:jvmTest`
Expected: PASS — 24 tests Expected: PASS — 24 tests
- [ ] **Step 5: Vérification manuelle desktop du geste** - [x] **Step 5: Vérification manuelle desktop du geste**
Run: `./gradlew :desktopApp:run` Run: `./gradlew :desktopApp:run`
Vérifier (fenêtre 480×800) : Vérifier (fenêtre 480×800) :
@@ -384,7 +384,7 @@ Vérifier (fenêtre 480×800) :
6. Vérifier que le scroll de la liste d'erreurs et les clics boutons fonctionnent toujours 6. Vérifier que le scroll de la liste d'erreurs et les clics boutons fonctionnent toujours
7. Vérifier qu'un swipe vertical dans la liste ne déclenche pas le retour 7. Vérifier qu'un swipe vertical dans la liste ne déclenche pas le retour
- [ ] **Step 6: Commit** - [x] **Step 6: Commit**
```bash ```bash
git add sharedUI/src/commonMain/kotlin/com/livrets/ui/SwipeBack.kt sharedUI/src/commonMain/kotlin/com/livrets/App.kt git add sharedUI/src/commonMain/kotlin/com/livrets/ui/SwipeBack.kt sharedUI/src/commonMain/kotlin/com/livrets/App.kt
@@ -21,11 +21,12 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.layout.onSizeChanged 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 kotlin.math.abs
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private const val SettleMillis = 200
object SwipeBackGesture { object SwipeBackGesture {
val EdgeZone = 24.dp val EdgeZone = 24.dp
const val CommitRatio = 0.4f const val CommitRatio = 0.4f
@@ -44,10 +45,8 @@ fun SwipeBack(
background: @Composable () -> Unit, background: @Composable () -> Unit,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val density = LocalDensity.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val offset = remember { Animatable(0f) } val offset = remember { Animatable(0f) }
val edgeZonePx = with(density) { SwipeBackGesture.EdgeZone.toPx() }
var widthPx by remember { mutableFloatStateOf(0f) } var widthPx by remember { mutableFloatStateOf(0f) }
val showBackground by remember { derivedStateOf { offset.value > 0f } } val showBackground by remember { derivedStateOf { offset.value > 0f } }
@@ -66,7 +65,7 @@ fun SwipeBack(
.pointerInput(Unit) { .pointerInput(Unit) {
awaitEachGesture { awaitEachGesture {
val down = awaitFirstDown() val down = awaitFirstDown()
if (down.position.x > edgeZonePx) return@awaitEachGesture if (down.position.x > SwipeBackGesture.EdgeZone.toPx()) return@awaitEachGesture
val tracker = VelocityTracker() val tracker = VelocityTracker()
tracker.addPosition(down.uptimeMillis, down.position) tracker.addPosition(down.uptimeMillis, down.position)
@@ -81,7 +80,7 @@ fun SwipeBack(
if (change.isConsumed && !change.pressed && change.previousPressed) { if (change.isConsumed && !change.pressed && change.previousPressed) {
if (dragging) { if (dragging) {
scope.launch { offset.animateTo(0f, tween(200)) } scope.launch { offset.animateTo(0f, tween(SettleMillis)) }
} }
break break
} }
@@ -116,7 +115,7 @@ fun SwipeBack(
if (change.changedToUpIgnoreConsumed()) { if (change.changedToUpIgnoreConsumed()) {
if (dragging) { if (dragging) {
val velocityDpPerSec = val velocityDpPerSec =
tracker.calculateVelocity().x / density.density tracker.calculateVelocity().x / density
val commit = SwipeBackGesture.shouldCommit( val commit = SwipeBackGesture.shouldCommit(
offsetPx = dragged, offsetPx = dragged,
containerWidthPx = widthPx, containerWidthPx = widthPx,
@@ -125,11 +124,11 @@ fun SwipeBack(
scope.launch { scope.launch {
if (commit) { if (commit) {
offset.snapTo(dragged) offset.snapTo(dragged)
offset.animateTo(widthPx, tween(200)) offset.animateTo(widthPx, tween(SettleMillis))
onBack() onBack()
} else { } else {
offset.snapTo(dragged) offset.snapTo(dragged)
offset.animateTo(0f, tween(200)) offset.animateTo(0f, tween(SettleMillis))
} }
} }
} }
@@ -49,4 +49,26 @@ class SwipeBackGestureTest {
) )
) )
} }
@Test
fun `pas de commit a la limite exacte de 40 pourcent sans velocite`() {
assertFalse(
SwipeBackGesture.shouldCommit(
offsetPx = 400f,
containerWidthPx = 1000f,
velocityDpPerSec = 0f,
)
)
}
@Test
fun `pas de commit avec un offset negatif`() {
assertFalse(
SwipeBackGesture.shouldCommit(
offsetPx = -100f,
containerWidthPx = 1000f,
velocityDpPerSec = 0f,
)
)
}
} }