Skip to content

Rewamped major aspects of the UI, to fit the official Youtube UI a bit more 😄 - #5986

Open
UmmmAGoodName wants to merge 22 commits into
yuliskov:masterfrom
UmmmAGoodName:stability-youtube-ui
Open

Rewamped major aspects of the UI, to fit the official Youtube UI a bit more 😄 #5986
UmmmAGoodName wants to merge 22 commits into
yuliskov:masterfrom
UmmmAGoodName:stability-youtube-ui

Conversation

@UmmmAGoodName

Copy link
Copy Markdown

A batch of UI work aimed at closing the gap between SmartTube and the official YouTube TV app, plus a set of stability fixes picked up along the way. Grouped by area below.

Stability

  • Fixed an unbounded ExoPlayer restart loop by adding exponential backoff and a give-up threshold (VideoLoaderController)
  • Fixed a few RxJava disposable leaks and an uncancellable postDelayed callback chain (ChannelPresenter, NavigateTitleView)
  • Cached repeated findViewById lookups in card/sidebar focus hot paths instead of re-querying on every bind/focus change
  • Assorted null-safety guards (empty video groups, negative selected positions, etc.)

Home/Browse

  • Duration/live/new badges are now a semi-transparent rounded pill instead of a flat opaque box
  • Video title/metadata now has real visual hierarchy (brighter title, dimmer metadata line)
  • Sidebar items get a YouTube-style pill highlight that hugs the icon+label instead of stretching full width, and the active section now stays highlighted after focus moves into the content grid
  • Reordered the default sidebar to match YouTube TV's current layout (new installs only, existing pinned order is untouched)
  • Removed the leftover teal box showing behind cards in the Teal color scheme

Search

  • Replaced the light search pill with a dark "glass" box, matching YouTube's search bar
  • Built an on-screen keyboard for the search screen instead of relying on the system IME
  • Suggestion chips are now rounded instead of square
  • Restored the magnifying-glass icon inside the search box (it had been hidden)

Player

  • Centered the primary transport controls instead of left-aligning them
  • Secondary controls (thumbs, subtitles, share, etc.) now sit inside a floating pill
  • Lightened the background scrim so it obscures less of the video
  • Moved the video title to the top-left of the screen instead of inline with the bottom controls
  • Play/pause now has a persistent white circle instead of blending into the other buttons
  • Fixed SponsorBlock segment markers being computed against a stale width and never recalculated on resize

Testing

Built and side-loaded (installStbetaDebug) after each change, checking focus navigation, card hover-preview, and playback on-device.

There might be some issues i've not caught in my testing, but please do point them out :)

claude and others added 22 commits July 15, 2026 18:25
… progress bar

Stability:
- Cap and back off the automatic ExoPlayer engine-restart loop so a
  persistent error (bad network, wrong clock) can't retry forever once a
  second (VideoLoaderController).
- Dispose ChannelPresenter's search/sort subscriptions instead of dropping
  them in unused locals.
- Cancel BrowseFragment's delayed empty-state check on view teardown, and
  guard getSelectedHeaderId() against no selection (-1).
- Guard VideoGroupObjectAdapter.removeAuthor() against an empty group.
- Cache NavigateTitleView's delayed icon-refresh Runnables so repeated
  posts reschedule instead of stacking, clean them up on detach, and cap
  the icon-load retry loop instead of polling forever.

UI:
- Give video card duration/live/new badges a rounded semi-transparent
  "pill" background instead of a flat rectangle, matching the official
  YouTube app, and preserve that shape when the badge color changes.
- Use the existing red progress-bar drawable for the default (Teal) and
  Blue color schemes' watched-progress indicator, matching YouTube's red
  progress bar (already used by the Red/Grey schemes).
VideoCardPresenter was always calling setBadgeColor() with an opaque
color (@color/black for the regular case), which overwrote the
semi-transparent pill background added for card_badge_pill.xml. Use the
semi-transparent card_badge_background color for the default badge case
so duration/shorts/etc. badges render as an actual translucent pill;
LIVE/NEW/upcoming badges keep their solid dark_red.
Bump the duration/badge pill to ~50% opacity (was ~70%) and a larger
corner radius so the rounded, semi-transparent look is unmistakable even
over a dark thumbnail corner, instead of reading as solid black.
- VideoCardPresenter set the exact same text color on both the title and
  the metadata line (channel * views * time ago), so they had identical
  brightness. Give the metadata line a dimmer color in both the default
  and selected states so the title reads as primary, YouTube style.
- Brighten/enlarge the shelf row titles ("Recommended", "Shorts") and use
  a medium-weight font instead of the default dim, regular-weight text,
  without touching the unrelated sidebar header style.
The sidebar (Home/Shorts/Trending/... nav) previously only dimmed/
brightened whole rows via alpha with no distinct highlight for the
currently active item, unlike the official app's white rounded "pill"
behind the focused nav entry with inverted (dark) icon/text.

- icon_header_item.xml: give the row a rounded-pill background (initially
  transparent) and breathing-room padding.
- IconHeaderItemPresenter: fade the pill in/out and crossfade the label
  and built-in icon color between white and near-black as the row's
  select level changes, mirroring the existing alpha animation. Remote
  icons (e.g. a pinned channel's own avatar) are explicitly left
  untinted since flattening them to a silhouette would make them
  unrecognizable.
The title/metadata strip under each thumbnail sat on a distinctly teal
box (card_default_background_dark, #004B53) in the default color
scheme. Official YouTube has no visible box there - text sits directly
on the dark shelf background. Use the same shelf_background_dark color
so the strip blends in, matching the Grey/Red schemes which already
use a near-black card background close to their own shelf color.
The pill background was applied to the full-width row container, so it
rendered as a wide bar spanning the entire sidebar instead of a content-
hugging capsule around the icon+label, unlike the official app. Move the
background/padding onto an inner wrap_content wrapper so the highlight
sizes itself to the content.
Leanback's RowHeaderPresenter only drives the pill/highlight off raw view
focus, so once the user moves into the video grid, no sidebar item stayed
highlighted at all - not even the section actually being viewed, unlike
the official app where the open section stays visually distinct.

- IconHeaderItemPresenter now tracks an explicit isActive flag per row
  (does this row's section match BrowsePresenter's current section?) and
  combines it with the focus-driven select level, so the pill/text/icon
  highlight persists for the active section regardless of focus.
- BrowseFragment re-binds the sidebar rows whenever the active section
  changes (header click, section-focus callback, or programmatic
  selectSection) so every row re-evaluates which one is now active.
setSelected() runs on every card during scrolling, and it was doing three
findViewById() calls (info_field, title_text, content_text) on every single
focus change. ComplexImageCardView now caches those references once in
init() and exposes setInfoAreaBackgroundColor/setTitleTextColor/
setContentTextColor so VideoCardPresenter and the card's own marquee/
lines-num/scroll-speed methods reuse them instead of re-querying the view
tree every time.

Also removes a similar redundant lookup in IconHeaderItemPresenter's
onBindViewHolder that I introduced earlier this session - IconViewHolder
already caches the icon/label views from onCreateViewHolder.
- ChannelCardPresenter (Subscriptions/Channels grid): cache
  channel_card_wrapper/channel_title/channel_image in a custom
  ViewHolder instead of re-running findViewById on every bind, and
  clear the Glide request on unbind so a stale thumbnail can't flash
  into a recycled card before its new image loads.
- UriBackgroundManager: lazily cache the video_surface view lookup
  instead of re-querying it on every debounced background update while
  scrolling. Cached lazily (not in the constructor) because this
  manager is built before setContentView() runs, so caching eagerly
  would permanently cache null.
- NavigateTitleView: tag each loadIcon() call with a token so a stale
  retry chain from an earlier call (e.g. onAccountChanged firing again
  while a previous chain is still waiting on layout) can't win a race
  and overwrite a newer icon load.
- ComplexImageView: cache the pending "finish old preview player"
  cleanup Runnable so a quick stop-then-start (fast re-focus during
  scrolling) runs it immediately instead of leaving the old and new
  preview players briefly alive at the same time.
Replaces reliance on the system IME popup for typing search queries with
a docked QWERTY keyboard, similar to the official YouTube app, instead of
following Google's stock Leanback template (a known, long-standing gap
called out in this project's own README FAQ).

- OnScreenKeyboardView: a widget wrapping a static QWERTY grid
  (view_onscreen_keyboard*.xml). Character keys need no per-key wiring -
  whatever text a key displays is the character it produces. Control keys
  (space/backspace/clear/search) are wired via ids.
- lb_search_fragment.xml (app-level override of the vendored Leanback
  layout): docks the keyboard below the search bar on the left, and
  shifts the results frame right to make room. Root stays a FrameLayout
  since SearchSupportFragment.onCreateView() casts it directly.
- SearchSupportFragment: wires keyboard key presses into the existing
  setSearchQuery(query, submit)/getSearchBarText() pipeline that already
  drives tag suggestions and results, so typing via the keyboard behaves
  identically to typing via the system IME.

The system keyboard path is left intact (not disabled) as a fallback if
the search field is still directly focused, e.g. for a Bluetooth
keyboard or accessibility tooling.
- Search input box was a light/white filled pill (a leftover from the
  stock Leanback template); switched it to a dark translucent "glass"
  box with a subtle border, matching YouTube's search field. Overrode
  the search text/hint colors to solid/translucent white so they read
  clearly against the new dark box (the previous colors were tuned for
  a light background).
- Search suggestion "tag" chips were flat rectangles; gave them a
  rounded pill shape like YouTube's suggestion chips, preserving the
  shape when the chip's color changes (focused vs default) the same
  way the video card badge fix did earlier this session.
The real YouTube TV app recently promoted Subscriptions and Library
access (history, playlists, your videos) right below Home, instead of
burying them after all the content-category rows (Shorts, Trending,
Kids, Sports, Live, Gaming, News, Music). Reorder SidebarService's
default section list the same way: Home, then Subscriptions/History/
Playlists/My Videos/Channels, then the content categories, then
Settings last.

Only affects fresh installs / accounts with no persisted sidebar order -
existing users' customized or already-persisted order is untouched,
since SidebarService only seeds from this default list when there's no
saved state yet.
First increment of the player screen redesign toward YouTube's new look:

- Play/pause/rewind/fast-forward/skip were left-aligned as a group;
  centered them as a standalone cluster instead.
- The secondary action buttons (thumbs up/down, subtitles, share, etc.)
  now sit inside a floating translucent "pill" capsule instead of a
  plain row, matching YouTube's grouped floating controls (SmartTube has
  many more secondary actions than YouTube's minimal set, so the pill
  holds more buttons here, but the visual style matches).
- Lightened the black scrim behind the controls overlay from 90% to 70%
  opacity, matching YouTube's move to obscure less of the video.
Previously the title/subtitle rendered inline with the bottom control
row (via PlaybackTransportRowPresenter's description presenter). Added
a new persistent title/subtitle view at the top-left of the screen
(top_title_dock in lb_playback_fragment.xml) and hid the old inline
block so it doesn't show twice.

VideoPlayerGlue.setTopTitleViews(dock, title, subtitle) wires the new
views in; its existing setTitle()/setSubtitle() overrides keep them in
sync with whatever the row's own title/subtitle are set to, and its
setControlsVisibility() override fades the new title in/out together
with the rest of the controls overlay, the same way it always did.
PlaybackFragment.initializeTopTitle() calls this once right after the
glue is created, mirroring the existing initializeGlobalClock()/
initializeGlobalEndingTime() pattern in the same file.
Play/pause was rendered identically to every other transport button
(rewind, fast-forward, skip, repeat, etc.), only gaining a focus ring
when navigated to - no visual priority over its neighbors, unlike the
official app where play/pause is a large, persistent white circle with
a dark icon.

ControlButtonPresenterSelector now special-cases PlaybackControlsRow.
PlayPauseAction: sets a solid white circle as the button's background
(layered behind the existing focus-ripple src, so focus feedback for
every button, including this one, is unaffected) and tints its icon
dark for contrast. Explicitly reset for every other action, both on
bind and unbind, since ViewHolders are recycled across different
actions as the row rebinds.
SeekBar.calculateSegments() caches segment rectangle positions using
getWidth() at the time setSegments() is called, but onSizeChanged()
never recomputed them on resize (that call was commented out) -- if
the view's width wasn't final yet the first time segments were set,
they'd be stuck at stale/zero-width positions forever.

Store the last segments list and recompute it from onSizeChanged() too,
and clear mSeekBarRectangles before repopulating (previously only done
on the null/clear path) so recalculating doesn't pile up duplicates.
The magnifying-glass badge inside the search bar was hidden (src="@null",
visibility="gone"), leaving the box with no icon at all and the mic/
settings/submit orbs floating disconnected around empty text. Restore
it using leanback's bundled lb_ic_in_app_search icon, tinted to match
the search text color so it reads correctly on the new dark box.
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