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(