Add a TV motion profile: Smooth or Snappy, chosen per box and overridable - #276
Conversation
…able After the shared TV focus tempo landed at 120ms (PR #272), the verdict from the Shield was "everything just feels so snappy instead of smooth" — and earlier, "my settings may not be for everyone". This introduces TvMotionProfile { smooth, snappy } so a television's motion is a choice, not one hard-coded tempo. Storage: services/tv_motion_profile.dart adds the pref (tv_motion_profile, owned/sanitised/copyable like the other style prefs) and TvMotionController, a synchronous ValueNotifier warmed in main() before runApp (like AppThemeController). Unset resolves to a per-device default: smooth on tvOS and on Android TV boxes reporting the NVIDIA Shield (manufacturer, model, board or device name), snappy on every other Android TV. Off television the profile is unread. Propagation: TvMotionScope (theme/tv_motion_scope.dart), an InheritedTheme installed beside AppThemeScope in MaterialApp.builder via a ValueListenableBuilder on the controller's notifier, so a chip press retargets every focus tween on the spot. Tokens: AppMotion gains `profile`, `tvFocusCurve`, `tvScroll`/`tvScrollCurve`, `scrollTempo`, `tvEntrance`/`entrance`, and `AppMotion.tvRoutesSharedAxis`. Snappy keeps every shipped figure byte-exact (fast focus, zero scroll-follow, the fast route fade, entrance none). Smooth is `base` (220ms) with the `emphasized` curve on TRANSFORM sites only (AnimatedScale — the ring/fill/ shadow tweens beside them stay on `standard`, since an overshooting curve on a lerped BoxDecoration or a clipping size asserts), a ~260ms scroll-follow, the shared-axis route transition, and the theme's entrance choreography. Reduced motion collapses both profiles to zero/none, and the TV raster policies (2.5px ring floor, no big blurs) are unchanged either way. Routed every TV Duration.zero ensureVisible call through AppMotion.tvScroll/scrollTempo: catalog_item_tile, episode_tile, tv_sidebar_nav, home_trakt_now_playing_card, youtube_video_card, iptv_channel_row, iptv_epg_panel, detail_identity/detail_layout_premium/ detail_layout_stage, export_channels_dialog, iptv_settings_two_pane, sidebar_customization_page, stremio_tv_tuner. Routed the curve on every TV AnimatedScale site through AppMotion.focusCurve: focus_expression, hover_grow, parallax_focus, detail_action_buttons, episode_tile, and the debrify_tv compact/focusable buttons. app_page_transitions now falls through to the shared-axis transition on TV under the smooth profile. section_reveal reads AppMotion.entrance instead of the raw token so a television's entrance choreography follows the profile too. Settings: Appearance -> Display -> "TV motion" (TV surface only), two inline chips via the existing SettingsRowKind.options / SettingsOptionRow machinery the Screen-layouts rows use, plus a search leaf per chip since the row has no page to open. Bumped SettingsTvLayout._kMaxCategoryRows to 21 to keep that pin honest with the one extra row. Tests: theme/tv_motion_profile_test.dart pins the device default, the pref, the controller and every AppMotion figure under both profiles, plus TvMotionScope propagation. settings_tv_motion_row_test.dart covers the catalog wiring: exactly one pref key on choice, TV Left/Right/Down, pane node release, and a chip press rebuilding a live AppMotion.tvFocus consumer. Ran and confirmed unchanged: shape_type_motion_test (TV policy pins), hover_grow_motion_test, focus_expression_motion_test, app_page_transitions_test, settings_appearance_groups_test, settings_layout_options_test, source_guard_test, storage_key_sweep_test, detail_showcase_test (cast-tap-to-actor feature untouched).
TvMotionController is read above ProfileGate via a ValueListenableBuilder, which rebuilds its own subtree on change but isn't recognized by the isolation guard's static scan for the addListener/setState contract every other above-the-gate controller (AppThemeController, TextBrightnessController) uses. Since the gate only rekeys its child, that scoped rebuild is otherwise correct at runtime, but switch to the same explicit addListener + setState pattern as the other two controllers for consistency and to satisfy the guard. tv_motion_profile stays profile-scoped: TvMotionController.warm() is explicitly re-run on every profile switch (see profile_app_lifecycle_participant.dart), and the pref is stored via ProfilePreferences like the other per-profile style settings, so a device-level de-scope would be wrong here.
|
CI fix: Determined Switched |
fix(tv-motion): route the pre-#276 scroll-follow sites through the TV motion profile
Why
After the shared TV focus tempo landed at 120ms (#272), the feedback from
the Shield was "everything just feels so snappy instead of smooth" — and
earlier, "my settings may not be for everyone". A television's motion
should be a choice, not one hard-coded tempo.
What
TvMotionProfile { smooth, snappy }with a per-device default: smoothon tvOS and on Android TV boxes reporting the NVIDIA Shield (by
manufacturer, model, board, or device name), snappy on every other
Android TV. Off television the profile is unread.
lib/services/tv_motion_profile.dart: the pref (tv_motion_profile,owned/sanitised/copyable like the other style prefs) and
TvMotionController— a synchronousValueNotifierwarmed inmain()before
runApp, likeAppThemeController.lib/theme/tv_motion_scope.dart:TvMotionScope, anInheritedThemeinstalled beside
AppThemeScopeinMaterialApp.buildervia aValueListenableBuilderon the controller's notifier, so a chip pressretargets every focus tween on the spot.
AppMotiongainsprofile,tvFocusCurve,tvScroll/tvScrollCurve,scrollTempo,tvEntrance/entrance, andtvRoutesSharedAxis. Snappykeeps every shipped figure byte-exact (fast focus, zero scroll-follow,
the fast route fade, entrance none). Smooth is
base(220ms) with theemphasizedcurve on TRANSFORM sites only — the ring/fill/shadow tweensbeside them stay on
standard, since an overshooting curve on a lerpedBoxDecorationor a clipping size asserts — a ~260ms scroll-follow, theshared-axis route transition, and the theme's entrance choreography.
Reduced motion collapses both profiles to zero/none, and the TV raster
policies (2.5px ring floor, no big blurs) are unchanged either way.
Duration.zeroensureVisiblecall throughAppMotion.tvScroll/scrollTempo:catalog_item_tile,episode_tile,tv_sidebar_nav,home_trakt_now_playing_card,youtube_video_card,iptv_channel_row,iptv_epg_panel,detail_identity/detail_layout_premium/detail_layout_stage,export_channels_dialog,iptv_settings_two_pane,sidebar_customization_page,stremio_tv_tuner.AnimatedScalesite throughAppMotion.focusCurve:focus_expression,hover_grow,parallax_focus,detail_action_buttons,episode_tile, and thedebrify_tvcompact/focusable buttons.app_page_transitionsfalls through to the shared-axis transition on TVunder the smooth profile.
section_revealreadsAppMotion.entranceinstead of the raw token so a television's entrance choreography follows
the profile too.
inline chips via the existing
SettingsRowKind.options/SettingsOptionRowmachinery the Screen-layouts rows already use, plusa search leaf per chip since the row has no page to open. Bumped
SettingsTvLayout._kMaxCategoryRowsto 21 to keep that pin honest withthe extra row.
How verified
theme/tv_motion_profile_test.dart(new, 24 tests): device default,the pref, the controller, every
AppMotionfigure under both profiles,TvMotionScopepropagation.settings_tv_motion_row_test.dart(new, 5 tests): catalog wiring —exactly one pref key on choice, TV Left/Right/Down, pane node release,
a chip press rebuilding a live
AppMotion.tvFocusconsumer.shape_type_motion_test(TV policy pins —entrance none under snappy, ring floor / blur cap unchanged),
hover_grow_motion_test,focus_expression_motion_test,app_page_transitions_test,settings_appearance_groups_test,settings_layout_options_test,source_guard_test,storage_key_sweep_test,detail_showcase_test(confirms thecast-tap-to-actor feature from Cast tiles open the actor's known-for titles #273 is untouched).
dart analyzeon every changed file: clean (same pre-existing infos asorigin, no new diagnostics).
tool/check_layering.dart: 42/42, nogrowth.
tool/analyze_baseline.py's reported "NEW diagnostics" arepre-existing SDK-drift noise, reproduced identically on a clean
origin/feature/post-refactorcheckout with none of this branch'sfiles implicated.
settings_tv_spotlight_layout_testanddebrify_tv_spotlight_dialog_testare pre-existing (Windows-renderedvs CI's Linux-rendered goldens) — reproduced identically on a clean
checkout; not touched here.
Risks
manufacturer/model/board/hardwareviadevice_info_plus, with the existing TVlistener's device-name channel as a fallback; an OEM reporting none of
the known Shield spellings falls back to snappy (the safe default) even
if it is Shield-class hardware.
emphasizedcurve is applied only to properties confirmed to bepure transforms at each of the routed sites; if a future edit adds a
decoration/size tween to one of those
AnimatedScales without noticingthe shared duration, it would need its own curve.