refactor: densité vivante, constante settle, tests de limite swipe-back
This commit is contained in:
3 files changed
+40
-19
No files matched your search
@@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user