Restore Home viewport across See all navigation
This commit is contained in:
parent
14a74ac43b
commit
9c8fceb18c
9 changed files with 125 additions and 45 deletions
|
|
@ -8,18 +8,19 @@ class HomeFocus(
|
|||
private val rows: List<HomeRow>,
|
||||
private val onAction: (HomeAction) -> Unit,
|
||||
) : ShellContentFocus {
|
||||
private val view = shell.viewFor("home")
|
||||
override val cardKey: String? get() = row?.let { if (it.kind == HomeRowKind.Shelf) it.cards.getOrNull(column)?.id ?: it.id else null }
|
||||
val row: HomeRow? get() = rows.getOrNull(shell.view.row)
|
||||
val column: Int get() = row?.let { shell.view.cards[it.id] ?: 0 } ?: 0
|
||||
val row: HomeRow? get() = rows.getOrNull(view.row)
|
||||
val column: Int get() = row?.let { view.cards[it.id] ?: 0 } ?: 0
|
||||
|
||||
fun focus(index: Int, column: Int) {
|
||||
shell.navFocused = false
|
||||
shell.view.row = index
|
||||
shell.view.home.focusedKey = rows[index].id
|
||||
view.row = index
|
||||
view.home.focusedKey = rows[index].id
|
||||
val target = rows[index]
|
||||
val bounded = column.coerceIn(0, (target.count - 1).coerceAtLeast(0))
|
||||
shell.view.cards[target.id] = bounded
|
||||
if (target.kind == HomeRowKind.Shelf) shell.view.home.firstCards[target.id] = target.adjustedFirst(bounded, shell.view.home.firstCards[target.id] ?: 0)
|
||||
view.cards[target.id] = bounded
|
||||
if (target.kind == HomeRowKind.Shelf) view.home.firstCards[target.id] = target.adjustedFirst(bounded, view.home.firstCards[target.id] ?: 0)
|
||||
}
|
||||
|
||||
override fun key(key: ShellKey): Boolean {
|
||||
|
|
@ -28,19 +29,19 @@ class HomeFocus(
|
|||
if (key == ShellKey.Ok) { activate(current); return true }
|
||||
if (reenterVisible()) return true
|
||||
when (key) {
|
||||
ShellKey.Left -> focus(shell.view.row, (column - 1).coerceAtLeast(0))
|
||||
ShellKey.Right -> focus(shell.view.row, (column + 1).coerceAtMost((current.count - 1).coerceAtLeast(0)))
|
||||
ShellKey.Left -> focus(view.row, (column - 1).coerceAtLeast(0))
|
||||
ShellKey.Right -> focus(view.row, (column + 1).coerceAtMost((current.count - 1).coerceAtLeast(0)))
|
||||
ShellKey.Up, ShellKey.Down -> {
|
||||
val direction = if (key == ShellKey.Up) -1 else 1
|
||||
var next = shell.view.row + direction
|
||||
var next = view.row + direction
|
||||
while (next in rows.indices && !rows[next].focusable) next += direction
|
||||
if (next in rows.indices) {
|
||||
val target = rows[next]
|
||||
val nearest = nearest(current, target)
|
||||
focus(next, shell.view.cards[target.id] ?: nearest)
|
||||
focus(next, view.cards[target.id] ?: nearest)
|
||||
} else if (direction < 0) {
|
||||
shell.view.row = rows.indexOfFirst { it.focusable }.coerceAtLeast(0)
|
||||
shell.view.home.resetScroll++
|
||||
view.row = rows.indexOfFirst { it.focusable }.coerceAtLeast(0)
|
||||
view.home.resetScroll++
|
||||
shell.navFocused = true
|
||||
shell.navIndex = shell.currentTab
|
||||
}
|
||||
|
|
@ -50,7 +51,6 @@ class HomeFocus(
|
|||
}
|
||||
|
||||
private fun reenterVisible(): Boolean {
|
||||
val view = shell.view
|
||||
val layout = view.listState.layoutInfo
|
||||
if (layout.totalItemsCount == 0 || view.home.positionedKey != row?.id || view.listState.isScrollInProgress) return false
|
||||
val density = layout.viewportSize.width / WolfskinTokens.Shell.stageWidth.value
|
||||
|
|
@ -79,9 +79,9 @@ class HomeFocus(
|
|||
|
||||
private fun nearest(from: HomeRow, to: HomeRow): Int {
|
||||
if (to.kind != HomeRowKind.Shelf) return (column * pitch(from) / pitch(to)).toInt()
|
||||
val fromFirst = if (from.kind == HomeRowKind.Shelf) shell.view.home.firstCards[from.id] ?: from.adjustedFirst(column, 0) else 0
|
||||
val fromFirst = if (from.kind == HomeRowKind.Shelf) view.home.firstCards[from.id] ?: from.adjustedFirst(column, 0) else 0
|
||||
val x = (column - fromFirst) * pitch(from) + from.cardWidth.value / 2
|
||||
val first = (shell.view.home.firstCards[to.id] ?: 0).coerceIn(0, (to.count - to.focusSlots).coerceAtLeast(0))
|
||||
val first = (view.home.firstCards[to.id] ?: 0).coerceIn(0, (to.count - to.focusSlots).coerceAtLeast(0))
|
||||
val last = minOf(to.count - 1, first + to.focusSlots - 1)
|
||||
var best = first
|
||||
var distance = Float.POSITIVE_INFINITY
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ fun HomeScreen(
|
|||
modifier: Modifier = Modifier,
|
||||
onAction: (HomeAction) -> Unit,
|
||||
) {
|
||||
val view = shell.view
|
||||
val view = shell.viewFor("home")
|
||||
LaunchedEffect(view, snapshot.savedAt) { view.savedAt = snapshot.savedAt }
|
||||
val action by rememberUpdatedState(onAction)
|
||||
val focus = remember(shell, snapshot.rows, view) { HomeFocus(shell, snapshot.rows) { action(it) } }
|
||||
|
|
@ -59,6 +59,14 @@ fun HomeScreen(
|
|||
LaunchedEffect(view.row, snapshot.rows) {
|
||||
snapshot.rows.drop(view.row + 1).firstOrNull { it.kind == HomeRowKind.Shelf && !it.skeleton }?.let { WolfskinImages.prefetch(context, it) }
|
||||
}
|
||||
LaunchedEffect(view) {
|
||||
if (view.home.positionedKey != null) {
|
||||
view.home.focusedKey?.let { view.home.tracks[it] }?.let { track ->
|
||||
track.scrollToItem(track.firstVisibleItemIndex, track.firstVisibleItemScrollOffset)
|
||||
}
|
||||
view.listState.scrollToItem(view.listState.firstVisibleItemIndex, view.listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(view, view.row, view.home.resetScroll, snapshot.rows) {
|
||||
val key = snapshot.rows.getOrNull(view.row)?.id
|
||||
if (view.home.positionedKey == key && view.home.positionedReset == view.home.resetScroll) return@LaunchedEffect
|
||||
|
|
|
|||
|
|
@ -8,13 +8,17 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
|
|
@ -38,11 +42,19 @@ fun HomeShelf(row: HomeRow, memory: HomeViewMemory, focused: Boolean, column: In
|
|||
val caption = if (row.cardKind == HomeCardKind.Poster) K.posterCaptionBand else WolfskinTokens.Shell.zero
|
||||
val first = row.adjustedFirst(column, memory.firstCards[row.id] ?: 0)
|
||||
SideEffect { if (memory.firstCards[row.id] != first) memory.firstCards[row.id] = first }
|
||||
val track = rememberLazyListState(first)
|
||||
val track = remember(memory, row.id) { memory.tracks.getOrPut(row.id) { LazyListState(first) } }
|
||||
val targetFirst by rememberUpdatedState(first)
|
||||
val density = LocalDensity.current
|
||||
LaunchedEffect(first) {
|
||||
val pitch = with(density) { (width + K.posterGap).toPx() }
|
||||
track.animateScrollBy((first - track.firstVisibleItemIndex) * pitch - track.firstVisibleItemScrollOffset, tween(WolfskinTokens.Motion.base))
|
||||
LaunchedEffect(track) {
|
||||
track.scrollToItem(track.firstVisibleItemIndex, track.firstVisibleItemScrollOffset)
|
||||
var positionedFirst = targetFirst
|
||||
snapshotFlow { targetFirst }.collectLatest { target ->
|
||||
if (target != positionedFirst) {
|
||||
positionedFirst = target
|
||||
val pitch = with(density) { (width + K.posterGap).toPx() }
|
||||
track.animateScrollBy((target - track.firstVisibleItemIndex) * pitch - track.firstVisibleItemScrollOffset, tween(WolfskinTokens.Motion.base))
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().height(L.rowHeadingBand + height + caption + L.rowGapAfter)) {
|
||||
if (!row.skeleton) Text(row.title, color = C.fg, style = T.rowHeading, maxLines = 1, modifier = Modifier.offset(x = L.safeSide))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
|
|
@ -9,6 +10,7 @@ import androidx.compose.runtime.setValue
|
|||
class HomeViewMemory {
|
||||
val ratings = HomeRatingCache()
|
||||
val firstCards = mutableStateMapOf<String, Int>()
|
||||
val tracks = mutableMapOf<String, LazyListState>()
|
||||
var loadingEntryRevision: Long? = null
|
||||
var focusedKey: String? = null
|
||||
var positionedKey: String? = null
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ fun LibraryScreen(
|
|||
cachedRatings: Set<String> = emptySet(),
|
||||
ratingsReachable: Boolean = true,
|
||||
offlineArt: ((String) -> ImageBitmap?)? = null,
|
||||
view: RememberedView = shell.view,
|
||||
) {
|
||||
val view = shell.view
|
||||
val density = LocalDensity.current
|
||||
val context = LocalContext.current
|
||||
val active = !shell.navFocused && !shell.pickerVisible
|
||||
|
|
|
|||
|
|
@ -68,7 +68,10 @@ class ShellState(
|
|||
private set
|
||||
val tabs: List<String> get() = if (person.hasAnime) ANIME_TABS else BASE_TABS
|
||||
val currentTab: Int get() = tabs.indexOf(rootRoute).coerceAtLeast(0)
|
||||
val view: RememberedView get() = views.getOrPut(person.key to route) { RememberedView() }
|
||||
val view: RememberedView get() = viewFor(route)
|
||||
|
||||
/** A composing destination must not borrow the previous route's scroller before its enter effect. */
|
||||
fun viewFor(destination: String): RememberedView = views.getOrPut(person.key to destination) { RememberedView() }
|
||||
|
||||
/** External intents and upstream pops remain authoritative over the screen. */
|
||||
fun enterDestination(destination: String) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue