mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
library: restore detail scroll positions after the rows load
The lazy list restores its saved position before a paging-backed playlist has replayed its cached pages, so a zero-item first frame clamped it back to the top. Track the position separately and reapply it once the item exists, which covers paged playlists as well as the album and artist lists. The test disposes and restores the scaffold with an empty first frame to keep this from regressing.
This commit is contained in:
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -47,12 +48,17 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -78,6 +84,7 @@ import com.subtracks.ui.theme.ArtworkTheme
|
||||
import com.subtracks.ui.theme.HeroGradient
|
||||
import com.subtracks.ui.theme.baseArtworkColors
|
||||
import com.subtracks.ui.theme.heroBarColor
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val FADE_DISTANCE_DP = 64
|
||||
@@ -103,7 +110,7 @@ fun HeroDetailScaffold(
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
|
||||
ArtworkTheme(effectiveArtwork) {
|
||||
BoxWithConstraints(modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
|
||||
val listState = rememberLazyListState()
|
||||
val listState = rememberRestorableLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val fill = rememberViewportFill(listState)
|
||||
val density = LocalDensity.current
|
||||
@@ -206,6 +213,32 @@ fun HeroDetailScaffold(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberRestorableLazyListState(): LazyListState {
|
||||
val state = rememberLazyListState()
|
||||
var index by rememberSaveable { mutableIntStateOf(state.firstVisibleItemIndex) }
|
||||
var offset by rememberSaveable { mutableIntStateOf(state.firstVisibleItemScrollOffset) }
|
||||
var pending by remember { mutableStateOf(index > 0) }
|
||||
LaunchedEffect(state) {
|
||||
if (pending) {
|
||||
state.requestScrollToItem(index, offset)
|
||||
snapshotFlow { state.layoutInfo.totalItemsCount }.first { it > index }
|
||||
state.scrollToItem(index, offset)
|
||||
pending = false
|
||||
}
|
||||
}
|
||||
LaunchedEffect(state) {
|
||||
snapshotFlow { state.firstVisibleItemIndex to state.firstVisibleItemScrollOffset }
|
||||
.collect { (currentIndex, currentOffset) ->
|
||||
if (!pending) {
|
||||
index = currentIndex
|
||||
offset = currentOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeroSubtitle(
|
||||
text: String,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.subtracks.ui.components
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.hasScrollAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class HeroDetailScaffoldRestoreTest {
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun aRestoredScreenWaitsForItsRowsBeforeRestoringTheScrollPosition() {
|
||||
var visible by mutableStateOf(true)
|
||||
var rows by mutableIntStateOf(100)
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
val holder = rememberSaveableStateHolder()
|
||||
if (visible) {
|
||||
holder.SaveableStateProvider("detail") {
|
||||
HeroDetailScaffold(
|
||||
artwork = null,
|
||||
title = "Album",
|
||||
onBack = {},
|
||||
header = { _, _ -> Box(Modifier.height(200.dp)) },
|
||||
content = { rowModifier -> rowItems(rows, rowModifier) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeRule.onNode(hasScrollAction()).performScrollToNode(hasText("Item 20"))
|
||||
composeRule.onNodeWithText("Item 20").assertIsDisplayed()
|
||||
|
||||
composeRule.runOnIdle { visible = false }
|
||||
composeRule.runOnIdle {
|
||||
rows = 0
|
||||
visible = true
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
composeRule.runOnIdle { rows = 100 }
|
||||
composeRule.waitForIdle()
|
||||
|
||||
composeRule.onNodeWithText("Item 20").assertIsDisplayed()
|
||||
}
|
||||
|
||||
private fun LazyListScope.rowItems(
|
||||
count: Int,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
repeat(count) { index ->
|
||||
item(key = index) {
|
||||
Box(modifier.height(48.dp)) {
|
||||
Text("Item $index")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user