Skip to content

feat(tv): - Rreduce D-pad focus-navigation lag and add keepVisible scroll mode for improved focus navigation - #2379

Open
usmanvrtx wants to merge 1 commit into
android_TV_implementationfrom
fix/improve-focus-navigation
Open

usmanvrtx wants to merge 1 commit into
android_TV_implementationfrom
fix/improve-focus-navigation

Conversation

@usmanvrtx

Copy link
Copy Markdown
Contributor

Description

This PR reduces D-pad focus-navigation lag in the TV focus runtime and adds an opt-in keepVisible scroll mode for focus reveal.

Focus navigation previously rebuilt its route/row/order view from every registered focus node on each D-pad press and resolved each target's FocusTraversalGroup with an ancestor walk. This PR caches that group at registration and maintains a route/row/order index so neighbour resolution is a direct lookup. It also adds styles.tvOptions.scrollMode: keepVisible, which reveals the focused item with the minimum movement using each scrollable's own geometry, like Flutter's default traversal.

Related Issue

N/A

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)

What Has Changed

Focus navigation performance

  • TVFocusTarget now captures its enclosing FocusTraversalGroup at registration, turning isInTraversalGroup into an O(1) identity check instead of a findAncestorWidgetOfExactType walk per target per D-pad press.
  • Added a route → row → order index to TVFocusRegistry, built on registration/unregistration with SplayTreeMaps so predecessor/successor (LEFT/RIGHT) and nearest-row (UP/DOWN) queries are logarithmic instead of full scans.
  • Added index lookup helpers: rowTargets, cellTarget, nextRow, previousRow, rowValues, hasCell, and clearRoute.
  • Re-registering a node now removes its previous index entry so stale row/order buckets do not accumulate.
  • The flat targets map is retained for backward compatibility with the legacy scan.
  • Added an opt-in includeDescendantScan argument to TVFocusOrderNode.collectInScope so callers that know every focusable registers a target can skip the app-wide live focus-tree walk. It defaults to true, so existing behavior is unchanged.

scrollMode: keepVisible

  • Added the TVScrollMode enum (legacy default, keepVisible) parsed from styles.tvOptions.scrollMode.
  • Added ensureWidgetVisible, which walks scrollable ancestors and applies each scrollable's own ScrollPosition.ensureVisible with keepVisibleAtStart/keepVisibleAtEnd. This respects slivers, pinned/overlay headers, content padding, and nested scrollables, and scrolls only when the item is actually offscreen.
  • The reveal policy is chosen from the item's current position and applied once per scrollable, avoiding a two-phase animation that reads as lag.
  • includeHorizontal/includeVertical let callers skip an axis managed elsewhere while continuing the ancestor walk.
  • Wired the new mode into the _TapEnabledWrapper and _TVFocusOnlyWrapper focus-scroll handlers. legacy remains the default.
  • keepVisible honors scrollAnimationDuration, scrollAnimationCurve, and resetScrollOnFocus; it ignores fixedFocusScroll, fixedFocusOffset, verticalScrollPadding, and horizontalScrollPadding, which stay legacy-only.
  • Extracted the shared _resetScrollOnFocus helper and added rememberActiveVerticalScrollableForContext.

Tests and documentation

  • Added test/widget/tv_focus_registry_test.dart covering the O(1) traversal-group check.
  • Added test/widget/tv_focus_registry_index_test.dart covering ordered row lookup, exact-cell lookup, row traversal, and index removal.
  • Added test/widget/tv_focus_scroll_test.dart covering the keepVisible reveal, the already-visible no-op, and excluded-axis behavior.
  • Documented scrollMode and the keepVisible semantics in doc/tv_developer_guide.md.

Usage

Row:
  styles:
    tvOptions:
      row: 3
      scrollMode: keepVisible # minimal, geometry-aware reveal

How to Test

  1. From the repository root, run melos bootstrap.

  2. From modules/ensemble, run the focused tests:

    flutter test test/widget/tv_focus_registry_test.dart
    flutter test test/widget/tv_focus_registry_index_test.dart
    flutter test test/widget/tv_focus_scroll_test.dart
  3. Run flutter analyze on the changed files:

    flutter analyze lib/framework/tv/tv_focus_order.dart \
      lib/framework/tv/tv_focus_registry.dart \
      lib/framework/tv/tv_focus_scroll.dart \
      lib/widget/helpers/box_wrapper.dart \
      lib/widget/helpers/controllers.dart
  4. On a TV/emulator, set styles.tvOptions.scrollMode: keepVisible on a scrollable row and confirm D-pad focus reveals offscreen items with minimal movement, that nested scrollables and pinned headers behave correctly, and that legacy remains the default when unset.

Screenshots / Videos

N/A

Checklist

  • I have run flutter analyze and addressed any new warnings
  • I have run flutter test and all tests pass
  • I have tested my changes on the relevant platform(s)
  • I have updated documentation if needed
  • My changes do not introduce new warnings or errors

- Rreduce D-pad focus-navigation lag in TV focus handling
- Introduce keepVisible scroll mode for improved focus navigation
@usmanvrtx usmanvrtx self-assigned this Sep 30, 2026
@usmanvrtx usmanvrtx changed the title feat(tv): feat(tv): - Rreduce D-pad focus-navigation lag and add keepVisible scroll mode for improved focus navigation Sep 30, 2026

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants