$npx -y skills add rcosteira79/android-skills --skill android-uxUse when designing or reviewing Android UI — applies Material Design 3 UX principles covering touch targets, spacing, navigation patterns, accessibility, animation timing, and platform conventions. Includes an M3 compliance audit that scores screens across 10 categories. Compleme
| 1 | # Android UX |
| 2 | |
| 3 | This reference covers the non-obvious platform facts and **reviewing** an existing screen for M3 compliance, not the basics of building M3-correct UI. |
| 4 | |
| 5 | ## Non-obvious platform facts |
| 6 | |
| 7 | ### Foldable postures |
| 8 | |
| 9 | Foldables introduce postures beyond window size classes. Detect them with `WindowInfoTracker` + `FoldingFeature` (Jetpack WindowManager), and **never place interactive content or critical information across the hinge**. |
| 10 | |
| 11 | | Posture | Detection | Layout behavior | |
| 12 | |---|---|---| |
| 13 | | Flat (unfolded) | no folding feature | Treat as Medium/Expanded by width | |
| 14 | | Half-opened, horizontal fold (tabletop) | `HALF_OPENED` + `Orientation.HORIZONTAL` | Content top half, controls bottom half | |
| 15 | | Half-opened, vertical fold (book) | `HALF_OPENED` + `Orientation.VERTICAL` | List left, detail right | |
| 16 | | Folded (cover screen) | — | Treat as Compact | |
| 17 | |
| 18 | ```kotlin |
| 19 | @Composable |
| 20 | fun FoldAwareLayout() { |
| 21 | val context = LocalContext.current |
| 22 | val layoutInfo by WindowInfoTracker.getOrCreate(context) |
| 23 | .windowLayoutInfo(context) // accepts @UiContext — Activity, InputMethodService, or createWindowContext() |
| 24 | .collectAsStateWithLifecycle(initialValue = WindowLayoutInfo(emptyList())) |
| 25 | |
| 26 | val fold = layoutInfo.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull() |
| 27 | when { |
| 28 | fold?.state == FoldingFeature.State.HALF_OPENED -> |
| 29 | if (fold.orientation == FoldingFeature.Orientation.HORIZONTAL) TabletopLayout() else BookLayout() |
| 30 | else -> StandardAdaptiveLayout() // by window size class |
| 31 | } |
| 32 | } |
| 33 | ``` |
| 34 | |
| 35 | ### M3 contrast levels (user-controlled contrast) |
| 36 | |
| 37 | Compose's `dynamicLightColorScheme` / `dynamicDarkColorScheme` read the system contrast setting automatically (SDK 34+) and expose **no** contrast parameter. To offer *in-app* contrast control (Standard 0.0 / Medium 0.5 / High 1.0 tonal distance), build the scheme from `Hct` + `SchemeContent`: |
| 38 | |
| 39 | ```kotlin |
| 40 | // com.google.android.material:material (MDC-Android), package com.google.android.material.color.utilities |
| 41 | // Marked @RestrictTo(LIBRARY_GROUP) — internal API, may change between versions; re-check on version bumps. |
| 42 | val hct = Hct.fromInt(0xFF6750A4.toInt()) |
| 43 | val scheme = SchemeContent(hct, /* isDark = */ false, /* contrastLevel = */ 1.0) |
| 44 | val colorScheme = lightColorScheme( |
| 45 | primary = Color(scheme.primary), |
| 46 | onPrimary = Color(scheme.onPrimary), |
| 47 | // ... map remaining roles |
| 48 | ) |
| 49 | ``` |
| 50 | |
| 51 | For multiplatform or a stable public API, `com.materialkolor:material-color-utilities` is a KMP alternative. |
| 52 | |
| 53 | ### Motion: duration tokens + reduced motion |
| 54 | |
| 55 | Pair M3's duration ladder with easing (`short*` → `FastOutSlowIn`; `medium*` → `Emphasized(De/Ac)celerate`; `long*`/`extraLong*` → `Emphasized`) rather than arbitrary millis. Reach for `MotionScheme` (Compose M3 1.4+) where available; otherwise hold durations at one source of truth in the theme. |
| 56 | |
| 57 | | Token group | Range | Typical use | |
| 58 | |---|---|---| |
| 59 | | `short1`…`short4` | 50–200ms | Micro-interactions, state changes (ripple, selection, switch) | |
| 60 | | `medium1`…`medium4` | 250–400ms | Standard transitions (screen enter/exit, expansion, reveal) | |
| 61 | | `long1`…`long4` | 450–600ms | Container transforms, fade-through between large surfaces | |
| 62 | | `extraLong1`…`extraLong4` | 700–1000ms | Shared-element / hero transitions on tablets/foldables | |
| 63 | |
| 64 | Animations must be interruptible and never block input. **Respect reduced motion** — Compose has no built-in `LocalReducedMotion`, so read `ANIMATOR_DURATION_SCALE` and provide one: |
| 65 | |
| 66 | ```kotlin |
| 67 | val LocalReducedMotion = staticCompositionLocalOf { false } |
| 68 | |
| 69 | @Composable |
| 70 | fun AppTheme(content: @Composable () -> Unit) { |
| 71 | val context = LocalContext.current |
| 72 | val reduceMotion = remember { |
| 73 | Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f |
| 74 | } |
| 75 | CompositionLocalProvider(LocalReducedMotion provides reduceMotion) { MaterialTheme { content() } } |
| 76 | } |
| 77 | |
| 78 | // at the call site |
| 79 | AnimatedVisibility( |
| 80 | visible = isVisible, |
| 81 | enter = if (LocalReducedMotion.current) EnterTransition.None else fadeIn() + slideInVertically(), |
| 82 | ) { Content() } |
| 83 | ``` |
| 84 | |
| 85 | (`staticCompositionLocalOf` + keyless `remember` read the setting once per Activity; for live updates switch to `compositionLocalOf` + a `ContentObserver` on `ANIMATOR_DURATION_SCALE`.) |
| 86 | |
| 87 | --- |
| 88 | |
| 89 | ## M3 Compliance Audit |
| 90 | |
| 91 | Use this when **reviewing** a screen or feature for Material Design 3 compliance — the part that's easy to skip from memory. Score each category **Pass / Partial / Fail**, then fix any Partial/Fail before shipping. |
| 92 | |
| 93 | ### 1. Color tokens |
| 94 | - All colors reference `MaterialTheme.colorScheme` roles — no hardcoded hex/ARGB. |
| 95 | - Corr |