Skip to content

[design-qa][P1] Motion and rendering cost: reveal stacked on the push, per-frame skeleton recomposition, animated values read in composition #176

Description

@cestercian

Part of the Sep-2026 designer's-eye QA of the 138 "Artistant iOS Light" screens (theme T13, category slow; epic #161). Full report with evidence and per-screen coverage: docs/DESIGN_QA_2026-09.md; the per-section appendices under docs/design-qa/ carry the code excerpts.

Why it matters. RevealOnAppear runs a second 300 ms animation on twelve screens after the 300 ms push; every skeleton block recomposes every frame; Done and Discover read animated values in composition; toasts ignore reduce-motion; tab roots reflow mid-slide; no cover photo ever fades in.

Fix direction. Reveal only on loading→loaded after the transition ends; graphicsLayer {} for animated alpha/scale/rotation; one hoisted skeleton transition; a shared ImageLoader with crossfade; motionTween everywhere.

Findings (14: P1 ×2, P2 ×3, P3 ×9)

  • F-CC-15 (P2) — Pushed screens animate twice: a 300 ms reveal stacked on the 300 ms push
    • Screens: 04, 05, 06, 16/50, 18/95/83/96/97, 22, 35, 46, 127 (pushed) and 10, 23, 36 (tab roots)
    • Where: feature/artist/ArtistProfileScreen.kt:369, feature/booking/BookingScreen.kt:126, feature/booking/CheckoutScreen.kt:125, feature/booking/BookingDetailScreen.kt:211, feature/score/ScoreExplainerScreen.kt:205, feature/gigs/GigRequestDetailScreen.kt:181, feature/availability/ManageAvailabilityScreen.kt:95, feature/wizard/WizardPublishSteps.kt:285, feature/profile/BlockedAccountsScreen.kt:125, feature/bookings/BookingsScreen.kt:209, feature/epk/EpkScreen.kt:243, feature/gigs/ArtistGigsScreen.kt:148; component designsystem/component/RevealOnAppear.kt
    • Fix: Reveal only when the screen transitions from Loading to Loaded AFTER the nav transition has finished (gate on !transition.isRunning or skip when data was available at first composition); never on tab roots.
  • F-CC-04 (P2) — Every skeleton block recomposes on every frame while loading
    • Screens: 59, 54 and every screen that draws SkeletonBlock/SkeletonPage (48 call sites)
    • Where: designsystem/component/Skeleton.kt:55, :68, :205-207
    • Fix: Hoist one transition per page (SkeletonPage) and apply it via graphicsLayer { alpha = pulse.value } so only the draw phase runs per frame.
  • F-CC-12 (P3) — Every cover photo pops in; nothing fades
    • Screens: 02, 03, 04, 10, 18, 23, 32, 41, 45
    • Where: the fifteen AsyncImage( sites in feature/discover/DiscoverScreen.kt, feature/search/SearchScreen.kt, feature/artist/ArtistProfileScreen.kt, feature/artist/ArtistProfileMedia.kt, feature/bookings/BookingsScreen.kt (×2), feature/booking/BookingDetailScreen.kt, feature/booking/BookingChrome.kt, feature/epk/EpkHub.kt (×2), feature/epk/EpkPanes.kt, feature/profile/ArtistListScreen.kt, feature/wizard/WizardMediaSteps.kt, feature/wizard/WizardPublishSteps.kt — none passes a crossfade, and no shared ImageLoader sets it
    • Fix: One app-level ImageLoader (Hilt-provided) with crossfade(AppTheme.motion.contentReveal), or a thin AppImage wrapper that every site uses.
  • F-AP-02 (P1) — RevealOnAppear stacks a 300 ms fade on the 300 ms push on 04 and 50
    • Screens: 04, 50
    • Where: feature/artist/ArtistProfileScreen.kt:369; feature/score/ScoreExplainerScreen.kt:205; designsystem/theme/Motion.kt:53, :66, :82; feature/artist/ArtistProfileMedia.kt:149, :173
    • Fix: drop RevealOnAppear on both screens (let the nav transition be the reveal), and move the chevron rotation into graphicsLayer { rotationZ = chevron }.
  • F-GS-22 (P3) — Done's pop-in reads an animated value in composition through Modifier.scale
    • Screens: 30
    • Where: feature/signup/DoneScreen.kt:73-78, :98
    • Fix: .graphicsLayer { scaleX = scale; scaleY = scale } (read the state inside the lambda).
  • F-DS-22 (P3) — Work in composition, and an animated rotation read in composition
    • Screens: 14, 15
    • Where: feature/search/SearchScreen.kt:377 (searchSuggestions(...) — filters facets and interleaves on every recomposition of BrowseSurface, not only when the query changes), :419 (eventTypes.chunked(2) allocated per recomposition); SearchFilterSheet.kt (chevron rotation)
    • Fix: remember(state.query, state.facets, state.results) { … }; remember { eventTypes.chunked(2) }; Modifier.graphicsLayer { rotationZ = rotation }.
  • F-BC-17 (P3) — Entry motion differs step to step, and 07 reflows once its read lands
    • Screens: 05, 06, 07
    • Where: feature/booking/BookingScreen.kt:126, CheckoutScreen.kt:125 (RevealOnAppear, contentReveal = medium2 = 300 ms per designsystem/theme/Motion.kt:53, :82) — absent on 07 / 94 / 132 / 17 / 61; ConfirmedScreen.kt:167-186
    • Fix: Drop RevealOnAppear on 05/06 (the nav transition already animates), and on 07 reserve the card with a Skeleton until the read resolves.
  • F-SH-11 (P2) — "Mark all read" flashes into the Activity header and shifts the title on entry
    • Screens: 123
    • Where: feature/system/ActivityScreen.kt:102, :142-155; feature/system/ActivityViewModel.kt:63, :130-144
    • Fix: set unreadOnArrival after log.markRead(unread) completes (or derive hasUnread from rows not in the arrival snapshot) so the control never appears for the marking pass.
  • F-SH-15 (P3) — The FAQ accordion animates on Compose defaults while its own chevron uses the tokens
    • Screens: 63
    • Where: feature/system/HelpCentreScreen.kt:233-240 vs :202-206
    • Fix: pass enter/exit built from motionTween(AppTheme.motion.contentReveal), matching the chevron's clock.
  • F-SH-19 (P3) — The toast's enter/exit ignores reduce-motion
    • Screens: 77
    • Where: designsystem/component/Toast.kt:105-112
    • Fix: swap both tween(...) calls for motionTween(AppTheme.motion.tabSwitch).
  • F-SH-21 (P3) — Tab-root content reflows the instant a push starts, mid-slide
    • Screens: global shell (both roles)
    • Where: navigation/ClientTabsScaffold.kt:216-217, :237; navigation/ArtistTabsScaffold.kt:166-167, :560-566
    • Fix: keep the bar composed and animate it out, or give TabPane a remembered bottom inset that only collapses once the transition settles. (Not device-verified — mechanism read from code.)
  • F-AS-04 (P1) — The clash warning scrolls away while Accept stays pinned
    • Screens: 35
    • Where: GigRequestDetailScreen.kt:351-353 inside the scroll column :194-227; dock at :229-265
    • Fix: hoist the clash banner out of the scroll column and render it directly above the dock (inside .dockSurface()), so it is pinned with the button it is warning about.
  • F-BN-18 (P3) — Copy-to-clipboard acknowledges itself for a different length of time than its twin
    • Screens: 18
    • Where: feature/booking/BookingDetailScreen.kt:158-164, :296-297 (COPIED_LABEL_MS = 1_600L) vs feature/epk/EpkPanes.kt:1051-1056, :1100 (COPIED_RESET_MS = 1_400L)
    • Fix: one constant in the design system, used by both.
  • F-PK-14 (P3) — "Copied" confirmations differ from the booking screen's
    • Screens: 23
    • Where: feature/epk/EpkPanes.kt:1076-1090, constant :1100; peer feature/booking/BookingDetailScreen.kt:649-653, constant :297
    • Fix: one shared duration constant and one casing convention (sentence case, matching the toast copy rule).

Acceptance

  • Every finding above is closed on the cited lines (or refuted in a comment with the reason).
  • ./gradlew :app:assembleDebug, :app:testDebugUnitTest and :app:lintDevDebug green.
  • The screens listed above walked on the artistant AVD with the debug harness (RELEASE.md §10) and the fix confirmed visually — this audit was code-grounded, not device-walked.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions