ui: cap the eased scroll and drop the snapshot focus map
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 symptom51077b8and5ea1ca5each 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 loop5ea1ca5fixed 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.
This commit is contained in:
parent
54dfe68726
commit
0ab19b835c
3 changed files with 31 additions and 5 deletions
|
|
@ -2,6 +2,8 @@ package net.jeena.jtubetv.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -469,7 +471,7 @@ private fun ChannelRow(
|
||||||
val notFullyVisible = target == null ||
|
val notFullyVisible = target == null ||
|
||||||
target.offset < 0 ||
|
target.offset < 0 ||
|
||||||
target.offset + target.size > rowState.layoutInfo.viewportEndOffset
|
target.offset + target.size > rowState.layoutInfo.viewportEndOffset
|
||||||
if (notFullyVisible) rowState.animateScrollToItem(focusedIndex)
|
if (notFullyVisible) rowState.easedScrollToItem(focusedIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (focused) {
|
if (focused) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package net.jeena.jtubetv.ui.screens
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
|
@ -450,7 +453,7 @@ fun FocusScrollRow(
|
||||||
val notFullyVisible = target == null ||
|
val notFullyVisible = target == null ||
|
||||||
target.offset < 0 ||
|
target.offset < 0 ||
|
||||||
target.offset + (target.size / 2) > rowState.layoutInfo.viewportEndOffset
|
target.offset + (target.size / 2) > rowState.layoutInfo.viewportEndOffset
|
||||||
if (notFullyVisible) rowState.animateScrollToItem(focusedIndex)
|
if (notFullyVisible) rowState.easedScrollToItem(focusedIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LazyRow(
|
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))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,14 @@ fun VideoListScreen(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val items = viewModel.items
|
val items = viewModel.items
|
||||||
// per-item focus requesters so BACK from the player can refocus the opened card
|
// per-item focus requesters so BACK from the player can refocus the opened card.
|
||||||
val requesters = androidx.compose.runtime.remember(items.size, epoch) {
|
// A plain HashMap, like the rows: this is/was written during composition of
|
||||||
androidx.compose.runtime.mutableStateMapOf<Int, FocusRequester>()
|
// 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<Int, FocusRequester>()
|
||||||
}
|
}
|
||||||
// Scroll position survives destination switches (per source), not just player exits.
|
// Scroll position survives destination switches (per source), not just player exits.
|
||||||
val gridState = androidx.compose.runtime.saveable.rememberSaveable(
|
val gridState = androidx.compose.runtime.saveable.rememberSaveable(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue