From 0ab19b835c0d97d8c347c3eadf5912d04db1f560 Mon Sep 17 00:00:00 2001 From: Jeena Date: Mon, 19 Oct 2026 13:08:18 +0000 Subject: [PATCH] ui: cap the eased scroll and drop the snapshot focus map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A system trace on the TV showed the eased focus scroll re-ticking for hundreds of milliseconds per move — animateScrollToItem's default spec scales with distance, and every tick redraws the whole window on a TV GPU, with 145 ms spikes when new cards composed mid-animation. That is what drags row scrolling back down to a few fps, again (same symptom 51077b8 and 5ea1ca5 each fixed a facet of). easedScrollToItem keeps the eased feel but caps the tween at ~140 ms, so a move costs a handful of cheap ticks instead of hundreds. The per-card focus requesters in the video grid were back in a snapshot state map written during composition — the exact recompose loop 5ea1ca5 fixed in the rows, still alive in the grid screens. Plain HashMap again, keyed on epoch only: keyed on items.size the map was dropped wholesale on every page load, and already-composed cards didn't re-populate it until their next recompose. --- .../jtubetv/ui/screens/ChannelsScreen.kt | 4 +++- .../net/jeena/jtubetv/ui/screens/HomeRows.kt | 21 ++++++++++++++++++- .../ui/screens/list/VideoListScreen.kt | 11 +++++++--- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt index 6dbba4c..7502c04 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt @@ -2,6 +2,8 @@ package net.jeena.jtubetv.ui.screens import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -469,7 +471,7 @@ private fun ChannelRow( val notFullyVisible = target == null || target.offset < 0 || target.offset + target.size > rowState.layoutInfo.viewportEndOffset - if (notFullyVisible) rowState.animateScrollToItem(focusedIndex) + if (notFullyVisible) rowState.easedScrollToItem(focusedIndex) } } if (focused) { diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeRows.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeRows.kt index f8a14a9..53d8625 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeRows.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeRows.kt @@ -1,5 +1,7 @@ package net.jeena.jtubetv.ui.screens +import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -450,7 +453,7 @@ fun FocusScrollRow( val notFullyVisible = target == null || target.offset < 0 || target.offset + (target.size / 2) > rowState.layoutInfo.viewportEndOffset - if (notFullyVisible) rowState.animateScrollToItem(focusedIndex) + if (notFullyVisible) rowState.easedScrollToItem(focusedIndex) } } LazyRow( @@ -495,3 +498,19 @@ fun FocusScrollRow( } } } + +/** + * Eased scroll to [index] with a capped duration (perf): [animateScrollToItem]'s + * default spec keeps re-ticking for hundreds of ms on long distances, and every + * tick is a full-window redraw on a TV GPU — which is what dragged the scroll + * down to a few fps. A short tween keeps the eased feel at a fraction of the + * ticks. (Shared by the home rows and the channels rows.) + */ +suspend fun LazyListState.easedScrollToItem(index: Int) { + val info = layoutInfo + val first = info.visibleItemsInfo.firstOrNull() ?: return + val stride = first.size + info.mainAxisItemSpacing + if (stride <= 0) return + val delta = index * stride - (first.index * stride + firstVisibleItemScrollOffset) + animateScrollBy(delta.toFloat(), tween(140)) +} diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/list/VideoListScreen.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/list/VideoListScreen.kt index dcd57d3..8e06582 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/list/VideoListScreen.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/list/VideoListScreen.kt @@ -74,9 +74,14 @@ fun VideoListScreen( ), ) { val items = viewModel.items - // per-item focus requesters so BACK from the player can refocus the opened card - val requesters = androidx.compose.runtime.remember(items.size, epoch) { - androidx.compose.runtime.mutableStateMapOf() + // per-item focus requesters so BACK from the player can refocus the opened card. + // A plain HashMap, like the rows: this is/was written during composition of + // every card, and a snapshot map here re-invalidated the reading scopes on + // each write — the exact recompose loop behind the old 2fps scroll (5ea1ca5). + // Keyed only on [epoch]: on items.size the map was dropped wholesale, and + // already-composed cards didn't re-populate it until their next recompose. + val requesters = androidx.compose.runtime.remember(epoch) { + HashMap() } // Scroll position survives destination switches (per source), not just player exits. val gridState = androidx.compose.runtime.saveable.rememberSaveable(