refactor(ui): BottomSheet sur anchoredDraggable (fling + tap fiable) #9

Closed
opened 2026-06-23 20:29:57 +02:00 by CyrilLeblanc · 0 comments
Owner

Contexte

Le drawer utilise un geste écrit à la main dans un seul pointerInput (ui/components/BottomSheet.kt:117-163). Deux défauts qui en découlent :

  • Pas d'inertie : au relâcher, la cible est choisie uniquement par la position vs collapseThresholdPx (:129-150), animée via un tween(300ms) fixe (:92). Aucune vélocité prise en compte → un flick rapide ne porte pas d'impulsion, on ne peut pas « lancer » le drawer pour l'ouvrir/fermer vite.
  • Clic avalé par le drag : down.consume() + isDragging = true immédiatement au down (:120-121), zéro slop. Le bouton « Itinéraire » (StopContent.kt:168-183, rendu via peekContent dans la Column qui porte le pointerInput) est donc intercepté dès la première frame ; sur les écrans haut FPS le micro-tremblement accumule des deltas (:152) → le tap est compté comme drag et le .clickable ne déclenche pas.

Comportement attendu

Remplacer le geste maison par anchoredDraggable (AnchoredDraggableState<SheetState> + DraggableAnchors), qui apporte nativement fling + discrimination tap/drag (slop).

  • Fling : un flick rapide ouvre/ferme le drawer en un geste (la vélocité décide de l'ancre cible, animation spring/decay native).
  • Tap fiable : un tap sur « Itinéraire » (et tout élément cliquable du header) déclenche son action, même sur haut FPS — le drag ne démarre qu'au-delà du touch slop.
  • Comportement préservé : états HIDDEN/PEEK/MID/EXPANDED, canHide (PEEK→HIDDEN hors EXPLORE), onHeightChange (padding carte), peek content custom.
  • Rendu visuel inchangé (coins arrondis, grabber).

Pistes d'implémentation

  • AnchoredDraggableState<SheetState>(initialValue = sheetState, …) + DraggableAnchors à 0 / peekPx / midPx / fullPx via state.updateAnchors(...) dans le BoxWithConstraints (ancres dépendent de la hauteur dispo + navBar).
  • Modifier.anchoredDraggable(state, orientation = Vertical) sur la Column du drawer, en lieu et place du pointerInput maison.
  • onHeightChange : réagir à state.requireOffset() (ou snapshotFlow sur l'offset) pour pousser la hauteur courante (drag + animation).
  • Synchro SheetState externe (sheetState / onStateChange) avec anchoredDraggableState.settledTarget / targetState.
  • Vérifier la dispo de l'API anchoredDraggable dans la BOM Compose actuelle (libs.versions.toml) ; monter la version si besoin.
  • Supprimer le pointerInput (:117-163), isDragging, dragHeightPx et la logique de snap manuelle.

Critères d'acceptation

  • Un flick vers le bas ferme rapidement le drawer (PEEK ou HIDDEN selon vélocité/position) ; un flick vers le haut l'étend.
  • Tap sur « Itinéraire » (header) fiable y compris sur haut FPS.
  • Les 4 états (HIDDEN/PEEK/MID/EXPANDED) et canHide fonctionnent comme avant.
  • onHeightChange continue de piloter le padding carte pendant le drag et l'animation.
  • Plus de pointerInput / isDragging / snap manuel dans BottomSheet.kt.
  • assembleDebug / assembleRelease OK ; pas de régression visuelle ni fonctionnelle (ETAs au drag, Plan).
## Contexte Le drawer utilise un geste écrit à la main dans un seul `pointerInput` (`ui/components/BottomSheet.kt:117-163`). Deux défauts qui en découlent : - **Pas d'inertie** : au relâcher, la cible est choisie uniquement par la position vs `collapseThresholdPx` (`:129-150`), animée via un `tween(300ms)` fixe (`:92`). Aucune vélocité prise en compte → un flick rapide ne porte pas d'impulsion, on ne peut pas « lancer » le drawer pour l'ouvrir/fermer vite. - **Clic avalé par le drag** : `down.consume()` + `isDragging = true` **immédiatement** au `down` (`:120-121`), **zéro slop**. Le bouton « Itinéraire » (`StopContent.kt:168-183`, rendu via `peekContent` dans la `Column` qui porte le `pointerInput`) est donc intercepté dès la première frame ; sur les écrans haut FPS le micro-tremblement accumule des deltas (`:152`) → le tap est compté comme drag et le `.clickable` ne déclenche pas. ## Comportement attendu Remplacer le geste maison par **`anchoredDraggable`** (`AnchoredDraggableState<SheetState>` + `DraggableAnchors`), qui apporte nativement fling + discrimination tap/drag (slop). - **Fling** : un flick rapide ouvre/ferme le drawer en un geste (la vélocité décide de l'ancre cible, animation spring/decay native). - **Tap fiable** : un tap sur « Itinéraire » (et tout élément cliquable du header) déclenche son action, même sur haut FPS — le drag ne démarre qu'au-delà du touch slop. - **Comportement préservé** : états HIDDEN/PEEK/MID/EXPANDED, `canHide` (PEEK→HIDDEN hors EXPLORE), `onHeightChange` (padding carte), peek content custom. - Rendu visuel inchangé (coins arrondis, grabber). ## Pistes d'implémentation - `AnchoredDraggableState<SheetState>(initialValue = sheetState, …)` + `DraggableAnchors` à 0 / peekPx / midPx / fullPx via `state.updateAnchors(...)` dans le `BoxWithConstraints` (ancres dépendent de la hauteur dispo + navBar). - `Modifier.anchoredDraggable(state, orientation = Vertical)` sur la Column du drawer, en lieu et place du `pointerInput` maison. - `onHeightChange` : réagir à `state.requireOffset()` (ou `snapshotFlow` sur l'offset) pour pousser la hauteur courante (drag + animation). - Synchro `SheetState` externe (`sheetState` / `onStateChange`) avec `anchoredDraggableState.settledTarget` / `targetState`. - Vérifier la dispo de l'API `anchoredDraggable` dans la BOM Compose actuelle (`libs.versions.toml`) ; monter la version si besoin. - Supprimer le `pointerInput` (`:117-163`), `isDragging`, `dragHeightPx` et la logique de snap manuelle. ## Critères d'acceptation - [ ] Un flick vers le bas ferme rapidement le drawer (PEEK ou HIDDEN selon vélocité/position) ; un flick vers le haut l'étend. - [ ] Tap sur « Itinéraire » (header) fiable y compris sur haut FPS. - [ ] Les 4 états (HIDDEN/PEEK/MID/EXPANDED) et `canHide` fonctionnent comme avant. - [ ] `onHeightChange` continue de piloter le padding carte pendant le drag et l'animation. - [ ] Plus de `pointerInput` / `isDragging` / snap manuel dans `BottomSheet.kt`. - [ ] `assembleDebug` / `assembleRelease` OK ; pas de régression visuelle ni fonctionnelle (ETAs au drag, Plan).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
CyrilLeblanc/gresit#9
No description provided.