# 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` toggled via `LaunchedEffect(Unit) { delay(StaggerDelayMs * index); visible = true }`. `AnimatedVisibility` has no built-in delay. ### Already-Animated Tracking (LazyColumn) Hoist a `mutableStateMapOf()` 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. - `animateFloatAsState` with `Bouncy` spring on scale modifier - `LaunchedEffect(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.pointerInput` press → `animateFloatAsState` 0.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. - `AnimatedVisibility` per item in LazyColumn with `slideInVertically + fadeIn` - `mutableStateMapOf` tracking (see shared section) - Applies to both `SwipeablePostCard` and search-mode `PostCard` - 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: `rememberInfiniteTransition` pulse alpha 0.6→1.0 - Status change: bounce scale + `animateColorAsState` - FAILED: shake via `Animatable` offset 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. - `AnimatedVisibility` wrapping the `if (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 `animateColorAsState` for chip selection (line 715) - Add `AnimatedVisibility` wrapper for the `if (!isSearchActive)` block (line 263) ### F10. Account switcher items (SLIDE/Gentle) — NEW Inside `AccountSwitcherBottomSheet` (line 954), account `ListItem`s 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. - `AnimatedVisibility` per grid item with `scaleIn` using `Bouncy` ### 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 `AnimatedVisibility` per section - PostStatsSection (line 307) already has internal `AnimatedVisibility` for 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 `WritingStatRow`s 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