mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
docs: update micro-animations spec after review
Address all review findings: fix Bouncy spring damping (0.55→0.65), add BouncyQuick for press feedback, document stagger pattern and already-animated tracking, add reduced motion support, fix nav transitions with popEnter/popExit, add disconnect confirmation dialog as new behavior, and add PulsingPlaceholder component.
This commit is contained in:
parent
31b04e549c
commit
163d6596c9
1 changed files with 111 additions and 49 deletions
|
|
@ -14,69 +14,112 @@ Central object in a new file `ui/animation/SwooshMotion.kt` providing reusable a
|
|||
|
||||
| Name | Type | Parameters | Use case |
|
||||
|------|------|-----------|----------|
|
||||
| `Bouncy` | Spring | dampingRatio=0.55, stiffness=400 | FAB, buttons, chips |
|
||||
| `Bouncy` | Spring | dampingRatio=0.65, stiffness=400 | FAB entrance, chips, badges |
|
||||
| `BouncyQuick` | Spring | dampingRatio=0.7, stiffness=1000 | Press feedback (settles in ~150ms) |
|
||||
| `Snappy` | Spring | dampingRatio=0.7, stiffness=800 | Expand/collapse, dialogs |
|
||||
| `Gentle` | Spring | dampingRatio=0.8, stiffness=300 | Cards, content reveal |
|
||||
| `Quick` | Tween | 200ms, FastOutSlowInEasing | Fade, color transitions |
|
||||
| `StaggerDelay` | Offset | 50ms per item | List item entrances |
|
||||
|
||||
### Reduced Motion Support
|
||||
|
||||
`SwooshMotion` exposes `val isReducedMotion: Boolean` that reads the system accessibility setting (`Settings.Global.ANIMATOR_DURATION_SCALE`). When `true`, all spring specs fall back to `snap()` (instant) and all tweens use 0ms duration. Every animation in this spec must route through `SwooshMotion` so the reduced-motion check is centralized.
|
||||
|
||||
### Stagger Pattern
|
||||
|
||||
For staggered entrance animations (#3, #13, #14), each item uses a local `MutableState<Boolean>` controlled by a `LaunchedEffect`:
|
||||
|
||||
```kotlin
|
||||
val visible = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
delay(SwooshMotion.StaggerDelay * index)
|
||||
visible.value = true
|
||||
}
|
||||
AnimatedVisibility(visible = visible.value, ...) { ... }
|
||||
```
|
||||
|
||||
`AnimatedVisibility` has no built-in delay parameter — this pattern is the canonical approach.
|
||||
|
||||
### Already-Animated Tracking (LazyColumn)
|
||||
|
||||
For staggered items inside `LazyColumn` (#3), items re-enter composition on scroll. To prevent replaying the entrance animation, hoist a `mutableStateSetOf<String>()` of already-animated item keys in `FeedScreen`:
|
||||
|
||||
```kotlin
|
||||
val animatedKeys = remember { mutableStateSetOf<String>() }
|
||||
// Inside each item:
|
||||
val shouldAnimate = post.id !in animatedKeys
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldAnimate) {
|
||||
delay(SwooshMotion.StaggerDelay * index)
|
||||
animatedKeys.add(post.id)
|
||||
}
|
||||
visible.value = true // instant if already animated
|
||||
}
|
||||
```
|
||||
|
||||
Only animate the first batch visible on initial load (cap at ~8 items). Items appended via infinite scroll appear without stagger.
|
||||
|
||||
## Feed Screen — 7 animations
|
||||
|
||||
### 1. FAB entrance (SPRING/Bouncy)
|
||||
When the feed screen opens, the FAB scales from 0 to 1 with `Bouncy` spring. Visible overshoot — the FAB "pops" onto screen.
|
||||
When the feed screen opens, the FAB scales from 0 to 1 with `Bouncy` spring. One visible overshoot — the FAB "pops" onto screen.
|
||||
- Compose API: `animateFloatAsState` with `Bouncy` spring on scale modifier
|
||||
- Trigger: `LaunchedEffect(Unit)` sets target to 1f
|
||||
|
||||
### 2. FAB press (SPRING/Bouncy)
|
||||
On tap, FAB shrinks to 85% and springs back to 100%.
|
||||
### 2. FAB press (SPRING/BouncyQuick)
|
||||
On tap, FAB shrinks to 85% and snaps back to 100%. Must settle in ~150ms so it completes before navigation fires.
|
||||
- Compose API: `Modifier.pointerInput` detecting press → `animateFloatAsState` scale 0.85f → 1f
|
||||
- Spring spec: `Bouncy`
|
||||
- Spring spec: `BouncyQuick` (dampingRatio=0.7, stiffness=1000)
|
||||
- Navigation fires immediately on tap — the spring animation is interrupted by the screen transition, which is fine
|
||||
|
||||
### 3. Post cards staggered entrance (SLIDE/Gentle)
|
||||
Cards slide in from bottom with cascading delay — 50ms per item. "Waterfall" effect.
|
||||
- Compose API: `AnimatedVisibility` per item with `slideInVertically` + `fadeIn`
|
||||
- Delay: `StaggerDelay * index` (capped at visible items, ~8 max)
|
||||
- Only on initial load, not on scroll-append
|
||||
- Compose API: `AnimatedVisibility` per item with `slideInVertically` + `fadeIn` using `Gentle` spring
|
||||
- Delay: Stagger pattern (see shared section). Capped at first ~8 visible items
|
||||
- Already-animated tracking via `animatedKeys` set (see shared section)
|
||||
- Items appended via infinite scroll appear without stagger (instant `visible = true`)
|
||||
|
||||
### 4. "Show more" expand (SPRING/Snappy)
|
||||
Card height animates with spring when text expands/collapses.
|
||||
- Compose API: `Modifier.animateContentSize(animationSpec = Snappy)`
|
||||
- Text content enters with `fadeIn`
|
||||
Card content transitions between truncated and full text with animated height change.
|
||||
- Compose API: `AnimatedContent(targetState = expanded)` with `fadeIn + expandVertically` / `fadeOut + shrinkVertically` as content transform
|
||||
- Height spring: `Snappy`
|
||||
- This replaces the current discrete text swap with a smooth animated transition
|
||||
|
||||
### 5. Empty state (FADE/Quick + scale)
|
||||
"No posts yet" icon and text fade in with subtle scale from 0.9 to 1.0.
|
||||
- Compose API: `AnimatedVisibility` with `fadeIn + scaleIn(initialScale = 0.9f)`
|
||||
|
||||
### 6. Queue status chip (BOUNCE/Bouncy)
|
||||
During upload: chip pulses (infinite alpha animation 0.6→1.0). On status change (success/fail): bounce scale + color crossfade.
|
||||
During upload: chip pulses (infinite alpha animation 0.6→1.0). On status change (success/fail): bounce scale + color crossfade. On FAILED: shake animation (horizontal offset oscillation) to draw attention.
|
||||
- Compose API: `rememberInfiniteTransition` for pulse; `animateColorAsState` + scale bounce on status change
|
||||
- Shake on FAILED: `Animatable` with `animateTo` offset -4dp → 4dp → 0dp (3 oscillations)
|
||||
- Color transition: `Quick` tween
|
||||
- Note: Each chip in UPLOADING state creates its own `rememberInfiniteTransition`. If many posts queue simultaneously, this is fine for ~5 items but could cause jank beyond that (unlikely in normal use)
|
||||
|
||||
### 7. Snackbar error (SLIDE/Snappy)
|
||||
Slides in from bottom with slight overshoot. Fades out on timeout.
|
||||
- Compose API: `AnimatedVisibility` with `slideInVertically(initialOffsetY = { it })` + `Snappy` spring
|
||||
- Exit: `fadeOut` with `Quick` tween
|
||||
|
||||
## Composer Screen — 6 animations
|
||||
## Composer Screen — 7 animations
|
||||
|
||||
### 8. Image preview (SCALE/Bouncy)
|
||||
After picking an image, the preview scales from 0 with bouncy spring. Close button ("X") rotates in (0°→360° with fade).
|
||||
After picking an image, the preview scales from 0 with bouncy spring. Close button fades in with scale.
|
||||
- Compose API: `AnimatedVisibility` with `scaleIn` using `Bouncy` spring
|
||||
- Close button: `animateFloatAsState` on rotation + `fadeIn`
|
||||
- Close button: `fadeIn + scaleIn(initialScale = 0.5f)` — no rotation (an "X" icon should feel stable and immediately tappable)
|
||||
|
||||
### 9. Link preview card (SLIDE/Gentle)
|
||||
After link loads, card slides up from below + fades in. While loading: shimmer placeholder.
|
||||
After link loads, card slides up from below + fades in. While loading: pulsing placeholder card.
|
||||
- Compose API: `AnimatedVisibility` with `slideInVertically + fadeIn` using `Gentle` spring
|
||||
- Shimmer: infinite `rememberInfiniteTransition` on a translucent gradient offset
|
||||
- Loading state: Replace current `LinearProgressIndicator` with a pulsing placeholder card using `rememberInfiniteTransition` on alpha (0.3→0.7). Simpler than a full shimmer gradient — just a fading placeholder shape
|
||||
|
||||
### 10. Schedule chip (SPRING/Bouncy)
|
||||
After picking date, chip pops in with bouncy spring. Clock icon subtly rotates.
|
||||
After picking date, chip pops in with bouncy spring.
|
||||
- Compose API: `AnimatedVisibility` with `scaleIn` using `Bouncy`
|
||||
- Icon: `animateFloatAsState` rotation 0→360
|
||||
|
||||
### 11. Publish button (BOUNCE/Bouncy)
|
||||
Subtle bounce on activation. During publishing: loading pulse (alpha animation). On success: checkmark icon scales in.
|
||||
- Compose API: Scale bounce on click via `animateFloatAsState`; `rememberInfiniteTransition` for pulse; `AnimatedContent` for icon swap with `scaleIn`
|
||||
### 11. Publish button (BOUNCE/BouncyQuick)
|
||||
Subtle bounce on activation. During publishing: loading pulse. On success: checkmark icon scales in.
|
||||
- Compose API: Scale bounce on click via `animateFloatAsState` with `BouncyQuick`; `rememberInfiniteTransition` for pulse; `AnimatedContent` for icon swap with `scaleIn`
|
||||
|
||||
### 12. Character counter color (FADE/Quick)
|
||||
Smooth color crossfade when exceeding 280 characters — neutral → red.
|
||||
|
|
@ -85,59 +128,75 @@ Smooth color crossfade when exceeding 280 characters — neutral → red.
|
|||
|
||||
### 13. Action buttons staggered entrance (SCALE/Gentle)
|
||||
Draft, Schedule, Publish buttons — cascading scale-in with 50ms delay each.
|
||||
- Compose API: `AnimatedVisibility` per button with `scaleIn` + `StaggerDelay * index`
|
||||
- Compose API: Stagger pattern (see shared section) with `AnimatedVisibility` + `scaleIn + fadeIn`
|
||||
|
||||
### NEW: 13b. Error text (SLIDE/Snappy)
|
||||
Composer error text slides in from left + fades in, consistent with snackbar style.
|
||||
- Compose API: `AnimatedVisibility` with `slideInHorizontally + fadeIn` using `Snappy` spring
|
||||
|
||||
## Detail Screen — 4 animations
|
||||
|
||||
### 14. Content reveal (FADE/Gentle)
|
||||
Elements appear sequentially: status badge → text → image → metadata. 80ms delay each.
|
||||
- Compose API: `AnimatedVisibility` per section with `fadeIn + slideInVertically(initialOffsetY = { 20 })`
|
||||
- Delay: `LaunchedEffect` with `delay(80 * index)`
|
||||
- Compose API: Stagger pattern (see shared section) with `AnimatedVisibility` per section using `fadeIn + slideInVertically(initialOffsetY = { 20 })`
|
||||
- Delay: 80ms per element (4 elements total = 320ms cascade)
|
||||
|
||||
### 15. Status badge entrance (SPRING/Bouncy)
|
||||
Badge scales from 0 with bounce — first visible element, draws attention.
|
||||
Badge scales from 0 with bounce — first visible element, draws attention. Part of the content reveal sequence (index 0, no delay).
|
||||
- Compose API: `animateFloatAsState` scale with `Bouncy` spring
|
||||
|
||||
### 16. Delete confirmation dialog (SCALE/Snappy)
|
||||
Dialog scales from center (0.8→1.0) with spring + backdrop fades in.
|
||||
- Compose API: Custom dialog wrapper with `scaleIn(initialScale = 0.8f)` + `fadeIn` backdrop
|
||||
- Compose API: Custom `AnimatedDialog` wrapper with `scaleIn(initialScale = 0.8f)` + `fadeIn` backdrop
|
||||
- Spring spec: `Snappy`
|
||||
|
||||
### 17. Metadata section (SLIDE/Gentle)
|
||||
Bottom metadata slides up — last in the reveal sequence.
|
||||
- Compose API: `AnimatedVisibility` with `slideInVertically` + `Gentle` spring
|
||||
Bottom metadata slides up — last in the content reveal sequence (index 3, 240ms delay).
|
||||
- Compose API: Part of stagger pattern in #14
|
||||
|
||||
## Settings Screen — 2 animations
|
||||
## Settings Screen — 3 animations
|
||||
|
||||
### 18. "Saved!" feedback (SPRING/Bouncy)
|
||||
Green "Saved!" text pops in with bounce (scale 0→1). After 2 seconds, fades out.
|
||||
### 18. "Settings saved" feedback (SPRING/Bouncy)
|
||||
Existing "Settings saved" text (currently static conditional) gets animated: pops in with bounce (scale 0→1). After 2 seconds, fades out.
|
||||
- Compose API: `AnimatedVisibility` with `scaleIn` using `Bouncy` spring; `LaunchedEffect` → `delay(2000)` → hide
|
||||
- Exit: `fadeOut` with `Quick` tween
|
||||
- Text and color remain as-is: "Settings saved" with `MaterialTheme.colorScheme.primary`
|
||||
|
||||
### 19. Disconnect dialog (FADE+SCALE/Snappy)
|
||||
Same pattern as delete dialog — scale from center + backdrop fade. "Disconnect" button has subtle red pulse.
|
||||
- Compose API: Same custom dialog wrapper as #16
|
||||
### 19. Disconnect confirmation dialog (NEW behavior + FADE+SCALE/Snappy)
|
||||
**New behavior:** Add a confirmation dialog before disconnect (currently the button directly clears credentials). The dialog uses the same `AnimatedDialog` wrapper as #16. "Disconnect" button has subtle red pulse.
|
||||
- Compose API: Same `AnimatedDialog` wrapper as #16
|
||||
- Red pulse: `rememberInfiniteTransition` on alpha of error color
|
||||
|
||||
### NEW: 19b. Disconnect dialog behavior
|
||||
The `OutlinedButton` currently calls `credentials.clear()` + `onLogout()` directly. After this change, it shows a confirmation dialog first. Confirm action triggers the existing clear + logout flow.
|
||||
|
||||
## Navigation Transitions — 5
|
||||
|
||||
Each `composable()` call in `NavGraph.kt` receives `enterTransition`, `exitTransition`, `popEnterTransition`, and `popExitTransition` lambdas. Back navigation uses `popEnterTransition`/`popExitTransition` (the reverse of the enter animation) — these are distinct parameters, not automatic.
|
||||
|
||||
### Feed → Composer
|
||||
Slide up from bottom + fade. Conceptually: FAB transforms into full screen.
|
||||
Slide up from bottom + fade.
|
||||
- `enterTransition = slideInVertically(initialOffsetY = { it }) + fadeIn()`
|
||||
- `exitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut()`
|
||||
- `exitTransition = fadeOut()`
|
||||
- `popEnterTransition = fadeIn()`
|
||||
- `popExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut()`
|
||||
|
||||
### Feed → Detail
|
||||
Slide in from right + fade. Post card expands into full view.
|
||||
Slide in from right + fade.
|
||||
- `enterTransition = slideInHorizontally(initialOffsetX = { it }) + fadeIn()`
|
||||
- `exitTransition = slideOutHorizontally(targetOffsetX = { it }) + fadeOut()`
|
||||
- `exitTransition = fadeOut()`
|
||||
- `popEnterTransition = fadeIn()`
|
||||
- `popExitTransition = slideOutHorizontally(targetOffsetX = { it }) + fadeOut()`
|
||||
|
||||
### Detail → Composer (via Edit)
|
||||
Same as Feed → Composer (slide up from bottom). The Composer always enters with the same transition regardless of source.
|
||||
|
||||
### Feed → Settings
|
||||
Standard slide from right.
|
||||
- `enterTransition = slideInHorizontally(initialOffsetX = { it })`
|
||||
- `exitTransition = slideOutHorizontally(targetOffsetX = { it })`
|
||||
|
||||
### Back (all screens)
|
||||
Reverse of the entry animation for each screen.
|
||||
- `exitTransition = fadeOut()`
|
||||
- `popEnterTransition = fadeIn()`
|
||||
- `popExitTransition = slideOutHorizontally(targetOffsetX = { it })`
|
||||
|
||||
### Setup → Feed
|
||||
Crossfade — smooth transition from animated setup background to feed.
|
||||
|
|
@ -149,24 +208,26 @@ Crossfade — smooth transition from animated setup background to feed.
|
|||
```
|
||||
ui/
|
||||
├── animation/
|
||||
│ └── SwooshMotion.kt # Shared animation specs object
|
||||
│ └── SwooshMotion.kt # Shared animation specs object + reduced motion check
|
||||
├── components/
|
||||
│ └── AnimatedDialog.kt # Reusable animated dialog wrapper (used by #16, #19)
|
||||
│ ├── AnimatedDialog.kt # Reusable animated dialog wrapper (used by #16, #19)
|
||||
│ └── PulsingPlaceholder.kt # Pulsing loading placeholder (used by #9)
|
||||
├── feed/
|
||||
│ └── FeedScreen.kt # Modified: #1-#7
|
||||
├── composer/
|
||||
│ └── ComposerScreen.kt # Modified: #8-#13
|
||||
│ └── ComposerScreen.kt # Modified: #8-#13, #13b
|
||||
├── detail/
|
||||
│ └── DetailScreen.kt # Modified: #14-#17
|
||||
├── settings/
|
||||
│ └── SettingsScreen.kt # Modified: #18-#19
|
||||
│ └── SettingsScreen.kt # Modified: #18-#19, #19b (new disconnect dialog)
|
||||
└── navigation/
|
||||
└── NavGraph.kt # Modified: navigation transitions
|
||||
└── NavGraph.kt # Modified: navigation transitions with enter/exit/popEnter/popExit
|
||||
```
|
||||
|
||||
## New files: 2
|
||||
## New files: 3
|
||||
- `ui/animation/SwooshMotion.kt`
|
||||
- `ui/components/AnimatedDialog.kt`
|
||||
- `ui/components/PulsingPlaceholder.kt`
|
||||
|
||||
## Modified files: 5
|
||||
- `FeedScreen.kt`, `ComposerScreen.kt`, `DetailScreen.kt`, `SettingsScreen.kt`, `NavGraph.kt`
|
||||
|
|
@ -176,3 +237,4 @@ ui/
|
|||
- Existing unit tests should pass unchanged (animations don't affect business logic)
|
||||
- Manual verification: each animation visually correct on emulator
|
||||
- Compose Preview for individual animated components where feasible
|
||||
- Test with system "Remove animations" setting enabled to verify reduced-motion fallback
|
||||
|
|
|
|||
Loading…
Reference in a new issue