9.9 KiB
Micro-Animations Design — Swoosh (v2)
Date: 2026-03-19 Status: Approved Style: Expressive & playful — bouncy springs, overshoot, lively feedback
Overview
Add 32 micro-animations + 8 navigation transitions across all screens to make the app feel alive. Create a shared SwooshMotion object with predefined animation specs ensuring consistent character throughout the app.
The app now includes: multi-account support, search with filters, swipe-to-dismiss, multi-image galleries, writing statistics dashboard, HTML preview, and hashtag support.
Shared Animation Specs — SwooshMotion
Central object in ui/animation/SwooshMotion.kt:
| Name | Type | Parameters | Use case |
|---|---|---|---|
Bouncy |
Spring | dampingRatio=0.65, stiffness=400 | FAB entrance, chips, badges |
BouncyQuick |
Spring | dampingRatio=0.7, stiffness=1000 | Press feedback (~150ms settle) |
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 |
StaggerDelayMs |
Long | 50 | List item entrances |
RevealDelayMs |
Long | 80 | Content reveal sequences |
Reduced Motion Support
SwooshMotion checks Settings.Global.ANIMATOR_DURATION_SCALE. When reduced motion is enabled, all springs fall back to snap() and all tweens use 0ms duration.
Stagger Pattern
Each item uses a MutableState<Boolean> toggled via LaunchedEffect(Unit) { delay(StaggerDelayMs * index); visible = true }. AnimatedVisibility has no built-in delay.
Already-Animated Tracking (LazyColumn)
Hoist a mutableStateMapOf<String, Boolean>() of already-animated item keys in FeedScreen. Check key !in animatedKeys before triggering entrance. Only first ~8 visible items on initial load animate. Items from infinite scroll appear instantly.
Feed Screen — 12 animations
F1. FAB entrance (SPRING/Bouncy)
Scale 0→1 with overshoot on screen open. Hidden during search mode.
animateFloatAsStatewithBouncyspring on scale modifierLaunchedEffect(Unit)sets target to 1f- FAB already conditionally hidden when
isSearchActive(line 249)
F2. FAB press (SPRING/BouncyQuick)
Shrinks to 85%, snaps back. Settles before navigation fires.
Modifier.pointerInputpress →animateFloatAsState0.85f→1f- Navigation fires immediately; animation interrupted by transition (fine)
F3. Post cards staggered entrance (SLIDE/Gentle)
Cascading slide-in from bottom, 50ms delay per item. "Waterfall" effect.
AnimatedVisibilityper item in LazyColumn withslideInVertically + fadeInmutableStateMapOf<String, Boolean>tracking (see shared section)- Applies to both
SwipeablePostCardand search-modePostCard - Capped at first 8 items, infinite scroll items appear instantly
F4. "Show more" expand (SPRING/Snappy)
AnimatedContent(targetState = expanded) with fadeIn + expandVertically / fadeOut + shrinkVertically. Replaces discrete text swap in PostCardContent.
F5. Empty states (FADE/Quick + scale)
All empty states (connection error, filter empty, search no results, normal empty) wrapped in AnimatedVisibility with fadeIn + scaleIn(0.9f).
F6. Queue status chip (BOUNCE/Bouncy)
- UPLOADING:
rememberInfiniteTransitionpulse alpha 0.6→1.0 - Status change: bounce scale +
animateColorAsState - FAILED: shake via
Animatableoffset oscillation (-4dp→4dp→0dp, 3 cycles)
F7. Snackbar (SLIDE/Snappy)
Both error snackbar (line 578) and pin confirmation snackbar (line 562) — AnimatedVisibility with slideInVertically + overshoot.
F8. Search bar (SLIDE/Quick) — NEW
SearchTopBar slides in from top with fade when search activates. Reverse on close.
AnimatedVisibilitywrapping theif (isSearchActive)branch (line 157)slideInVertically(initialOffsetY = { -it }) + fadeIn/ reverse
F9. Filter chips bar (FADE/Quick) — NEW
FilterChipsBar fades in/out when toggling between search and normal mode.
- Already has
animateColorAsStatefor chip selection (line 715) - Add
AnimatedVisibilitywrapper for theif (!isSearchActive)block (line 263)
F10. Account switcher items (SLIDE/Gentle) — NEW
Inside AccountSwitcherBottomSheet (line 954), account ListItems get staggered slide-in.
- ModalBottomSheet has built-in slide animation
- Add stagger pattern to account items inside the sheet
F11. Pinned section header (FADE/Quick) — NEW
"📌 Pinned" header at line 790 — AnimatedVisibility with fadeIn + slideInVertically when pinned posts exist.
F12. Account switch overlay (FADE/Quick) — NEW
"Switching account..." overlay (line 288) — crossfade entrance with scale on the spinner.
Composer Screen — 9 animations
C1. Image grid preview (SCALE/Bouncy)
ImageGridPreview (line 556) — each image thumbnail scales in with bounce when added. Scale-out on removal.
AnimatedVisibilityper grid item withscaleInusingBouncy
C2. Link preview card (SLIDE/Gentle)
After link loads, card slides up + fades in. Loading: PulsingPlaceholder replaces LinearProgressIndicator (line 317).
C3. Schedule chip (SPRING/Bouncy)
Chip at line 365 pops in with scaleIn using Bouncy when state.scheduledAt != null.
C4. Publish button (BOUNCE/BouncyQuick)
Button at line 421 — bounce on click, loading pulse during submit, AnimatedContent icon swap to checkmark on success.
C5. Character counter color (FADE/Quick)
Three-tier animateColorAsState: onSurfaceVariant → tertiary (>280) → error (>500). Matches current logic at line 211.
C6. Action buttons staggered (SCALE/Gentle)
Publish button (line 421) + Row of [Draft, Schedule] (line 433). Two-step stagger: button first, then row.
C7. Error text (SLIDE/Snappy)
Error at line 407 — AnimatedVisibility with slideInHorizontally + fadeIn.
C8. Hashtag chips (SPRING/Bouncy) — NEW
Extracted tags FlowRow (line 225) — staggered scaleIn per chip as tags appear/change.
C9. Edit/Preview crossfade (FADE/Quick) — NEW
Crossfade(targetState = state.isPreviewMode) wrapping the if (state.isPreviewMode) branch (line 167). Smooth transition between edit and preview modes.
Detail Screen — 5 animations
D1. Content reveal (FADE/Gentle)
Sequential: status row → text → tags → image gallery → link preview → PostStatsSection. 80ms delay each.
- Stagger pattern with
AnimatedVisibilityper section - PostStatsSection (line 307) already has internal
AnimatedVisibilityfor expand/collapse
D2. Status badge entrance (SPRING/Bouncy)
First in reveal sequence (index 0). Scale-in with bounce.
D3. Delete dialog (SCALE/Snappy)
Replace AlertDialog at line 312 with AnimatedDialog wrapper.
D4. PostStatsSection (SLIDE/Gentle)
Last in reveal sequence. Slides up. Internal expand/collapse already animated.
D5. Pin toggle feedback (BOUNCE/Bouncy) — NEW
Pin icon in TopAppBar (line 78 area) — bouncy scale pulse when toggling featured state. animateFloatAsState scale 1→1.2→1.
Settings Screen — 3 animations
S1. Account card (FADE/Quick) — NEW
Current account Card (line 78) — fade-in with subtle scale on screen entry.
S2. Disconnect confirmation dialog (SCALE/Snappy)
New behavior: Add confirmation dialog before both "Disconnect Current Account" (line 139) and "Disconnect All Accounts" (line 165). Uses AnimatedDialog wrapper.
S3. Theme chip selection (SPRING/Bouncy) — NEW
In ThemeModeSelector (line 184), selected chip gets a brief bouncy scale pulse on selection change.
Stats Screen — 3 animations (all NEW)
ST1. Stats cards staggered entrance (SCALE/Bouncy)
Four StatsCard composables (lines 68-98) — staggered scale-in with bounce. 50ms delay per card.
ST2. Writing stats reveal (SLIDE/Gentle)
OutlinedCard at line 109 — slide-up with fade after cards complete. Internal WritingStatRows cascade.
ST3. Number count-up (FADE/Quick)
Stat values animate from 0 to target via animateIntAsState. Subtle but adds life to the dashboard.
Navigation Transitions — 8 routes
Each composable() receives enterTransition, exitTransition, popEnterTransition, popExitTransition.
| Route | Enter | Pop Exit |
|---|---|---|
| Setup | fadeIn(500ms) | — (inclusive popUpTo) |
| Feed | fadeIn(300ms) | fadeOut(200ms) |
| Composer | slideInVertically + fadeIn | slideOutVertically + fadeOut |
| Detail | slideInHorizontally + fadeIn | slideOutHorizontally + fadeOut |
| Settings | slideInHorizontally | slideOutHorizontally |
| Stats | slideInHorizontally | slideOutHorizontally |
| Preview | slideInVertically + fadeIn | slideOutVertically + fadeOut |
| AddAccount | slideInVertically + fadeIn | slideOutVertically + fadeOut |
File Structure
ui/
├── animation/
│ └── SwooshMotion.kt # Shared specs + reduced motion
├── components/
│ ├── AnimatedDialog.kt # Scale-in dialog wrapper
│ └── PulsingPlaceholder.kt # Pulsing loading placeholder
├── feed/
│ └── FeedScreen.kt # F1-F12
├── composer/
│ └── ComposerScreen.kt # C1-C9
├── detail/
│ └── DetailScreen.kt # D1-D5
├── settings/
│ └── SettingsScreen.kt # S1-S3
├── stats/
│ └── StatsScreen.kt # ST1-ST3
└── navigation/
└── NavGraph.kt # 8 route transitions
New files: 3 (SwooshMotion.kt, AnimatedDialog.kt, PulsingPlaceholder.kt)
Modified files: 6 (FeedScreen.kt, ComposerScreen.kt, DetailScreen.kt, SettingsScreen.kt, StatsScreen.kt, NavGraph.kt)
Testing Strategy
- Existing unit tests pass unchanged (animations don't affect business logic)
- Manual verification on emulator per animation
- Test with "Remove animations" accessibility setting for reduced-motion fallback