Skip to content

Commit 002d437

Browse files
Treehugger RobotGerrit Code Review
authored andcommitted
Merge "CacheWindow refill update #2" into androidx-main
2 parents c640193 + fdc614d commit 002d437

5 files changed

Lines changed: 182 additions & 28 deletions

File tree

compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListCacheWindowTest.kt

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class LazyListCacheWindowTest(orientation: Orientation) :
185185
}
186186

187187
@Test
188-
fun datasetChanged_shouldMakeSureNestedItemsChanged() {
188+
fun datasetChanged_shouldMakeSureNestedItemsChanged_afterScroll() {
189189
val items = mutableStateOf(listOf("a", "b", "c", "d", "e"))
190190

191191
rule.setContent {
@@ -264,6 +264,80 @@ class LazyListCacheWindowTest(orientation: Orientation) :
264264
rule.onNodeWithTag("second-nested-e").assertExists() // nested prefetched
265265
}
266266

267+
@Test
268+
fun datasetChanged_shouldMakeSureNestedItemsChanged_noScroll() {
269+
val items = mutableStateOf(listOf("a", "b", "c", "d"))
270+
271+
rule.setContent {
272+
@OptIn(ExperimentalFoundationApi::class)
273+
state = rememberLazyListState(cacheWindow = viewportWindow)
274+
LazyColumnOrRow(
275+
Modifier.mainAxisSize(itemsSizeDp * 2f)
276+
.then(
277+
object : RemeasurementModifier {
278+
override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
279+
remeasure = remeasurement
280+
}
281+
}
282+
),
283+
state,
284+
) {
285+
items(items.value, key = { it }) {
286+
if (it == "e" || it == "f") {
287+
val state = rememberLazyListState(cacheWindow = viewportWindow)
288+
LazyRow(
289+
Modifier.mainAxisSize(itemsSizeDp).fillMaxCrossAxis().testTag(it),
290+
state = state,
291+
) {
292+
item {
293+
Spacer(
294+
Modifier.mainAxisSize(itemsSizeDp)
295+
.fillMaxCrossAxis()
296+
.testTag("first-nested-$it")
297+
)
298+
}
299+
300+
item {
301+
Spacer(
302+
Modifier.mainAxisSize(itemsSizeDp)
303+
.fillMaxCrossAxis()
304+
.testTag("second-nested-$it")
305+
)
306+
}
307+
}
308+
} else {
309+
Spacer(
310+
Modifier.mainAxisSize(itemsSizeDp)
311+
.fillMaxCrossAxis()
312+
.testTag(it)
313+
.layout { measurable, constraints ->
314+
val placeable = measurable.measure(constraints)
315+
layout(placeable.width, placeable.height) {
316+
placeable.place(0, 0)
317+
}
318+
}
319+
)
320+
}
321+
}
322+
}
323+
}
324+
325+
rule.onNodeWithTag("a").assertIsDisplayed() // fully visible
326+
rule.onNodeWithTag("b").assertIsDisplayed() // fully visible
327+
rule.onNodeWithTag("c").assertExists() // part of the window
328+
rule.onNodeWithTag("d").assertExists() // part of the window
329+
println("Changing Dataset")
330+
rule.runOnIdle { items.value = listOf("a", "b", "e", "f", "g", "h") }
331+
rule.waitForIdle()
332+
333+
rule.onNodeWithTag("e").assertExists() // item e will take place of item c
334+
rule.onNodeWithTag("first-nested-e").assertExists() // nested prefetched
335+
rule.onNodeWithTag("second-nested-e").assertExists() // nested prefetched
336+
rule.onNodeWithTag("f").assertExists() // item f will take place of item d
337+
rule.onNodeWithTag("first-nested-f").assertExists() // nested prefetched
338+
rule.onNodeWithTag("second-nested-f").assertExists() // nested prefetched
339+
}
340+
267341
@Test
268342
fun datasetChanged_noScrollHappened_shouldKeepAroundWithinBounds_notCrash() {
269343
val numItems = mutableStateOf(100)

compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListCacheWindowStrategy.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ internal class LazyListCacheWindowScope() : CacheWindowScope {
135135

136136
override fun getLastIndexInLine(lineIndex: Int): Int = lineIndex
137137

138+
override fun getVisibleLineKey(indexInVisibleLines: Int): Any {
139+
return layoutInfo.visibleItemsInfo[indexInVisibleLines].key
140+
}
141+
138142
override fun getLastLineIndex(): Int {
139143
if (totalItemsCount == 0) return InvalidIndex
140144
return totalItemsCount - 1

compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridCacheWindowStrategy.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis
2121
import androidx.compose.foundation.gestures.snapping.sizeOnMainAxis
2222
import androidx.compose.foundation.lazy.layout.CacheWindowLogic
2323
import androidx.compose.foundation.lazy.layout.CacheWindowScope
24+
import androidx.compose.foundation.lazy.layout.CachedItem
2425
import androidx.compose.foundation.lazy.layout.InvalidIndex
2526
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
2627
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle
@@ -137,6 +138,15 @@ private class LazyGridCacheWindowScope() : CacheWindowScope {
137138
return tallestItemSize
138139
}
139140

141+
override fun getVisibleLineKey(indexInVisibleLines: Int): Any {
142+
// using the first item key to represent this line.
143+
val laneIndex = indexInVisibleLines + firstVisibleLineIndex
144+
return layoutInfo.visibleItemsInfo
145+
.fastFilter { it.lineIndex == laneIndex }
146+
.firstOrNull()
147+
?.key ?: CachedItem.NoKey
148+
}
149+
140150
override fun getVisibleItemLine(indexInVisibleLines: Int): Int =
141151
firstVisibleLineIndex + indexInVisibleLines
142152

compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/CacheWindowLogic.kt

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package androidx.compose.foundation.lazy.layout
1818

19-
import androidx.collection.mutableIntIntMapOf
2019
import androidx.collection.mutableIntObjectMapOf
2120
import androidx.collection.mutableIntSetOf
2221
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -43,7 +42,7 @@ internal abstract class CacheWindowLogic(
4342
* Cache for items sizes in the current window. Holds sizes for both visible and non-visible
4443
* items
4544
*/
46-
private val windowCache = mutableIntIntMapOf()
45+
private val windowCache = mutableIntObjectMapOf<CachedItem>()
4746
private var previousPassDelta = 0f
4847
private var previousPassItemCount = UnsetItemCount
4948
private var hasUpdatedVisibleItemsOnce = false
@@ -120,15 +119,7 @@ internal abstract class CacheWindowLogic(
120119
* changed.
121120
*/
122121
if (previousPassItemCount != UnsetItemCount && previousPassItemCount != totalItemsCount) {
123-
debugLog { "Total Items Changed" }
124-
shouldRefillWindow = true
125-
prefetchWindowStartLine = prefetchWindowStartLine.coerceAtLeast(0)
126-
val lastLineIndex = getLastLineIndex()
127-
if (lastLineIndex != InvalidIndex) {
128-
prefetchWindowEndLine = prefetchWindowEndLine.coerceAtMost(lastLineIndex)
129-
}
130-
/** Free up the space so the fill will happen and not re-use old data. */
131-
removeOutOfBoundsItems(prefetchWindowEndLine, itemsCount - 1)
122+
onDatasetChangedSize()
132123
}
133124

134125
itemsCount = totalItemsCount
@@ -137,8 +128,8 @@ internal abstract class CacheWindowLogic(
137128
// by [cancelOutOfBounds]. If any items changed sizes we re-trigger the window filling
138129
// update.
139130
if (hasVisibleItems) {
140-
forEachVisibleItem { index, mainAxisSize ->
141-
if (index != InvalidIndex) cacheVisibleItemsInfo(index, mainAxisSize)
131+
forEachVisibleItem { index, key, mainAxisSize ->
132+
if (index != InvalidIndex) cacheVisibleItemsInfo(index, key, mainAxisSize)
142133
}
143134
if (shouldRefillWindow) {
144135
// refill window in accordance with last pass delta
@@ -234,6 +225,25 @@ internal abstract class CacheWindowLogic(
234225
}
235226
}
236227

228+
private fun CacheWindowScope.onDatasetChangedSize() {
229+
debugLog { "Total Items Changed" }
230+
shouldRefillWindow = true
231+
prefetchWindowStartLine = prefetchWindowStartLine.coerceAtLeast(0)
232+
val lastLineIndex = getLastLineIndex()
233+
if (lastLineIndex != InvalidIndex) {
234+
prefetchWindowEndLine = prefetchWindowEndLine.coerceAtMost(lastLineIndex)
235+
}
236+
237+
/**
238+
* Resets the window state. We will refill the window on the direction of the last scroll.
239+
*/
240+
if (previousPassDelta <= 0f) {
241+
removeOutOfBoundsItems(lastVisibleLineIndex, itemsCount - 1)
242+
} else {
243+
removeOutOfBoundsItems(0, firstVisibleLineIndex)
244+
}
245+
}
246+
237247
fun resetStrategy() {
238248
prefetchWindowStartLine = Int.MAX_VALUE
239249
prefetchWindowEndLine = Int.MIN_VALUE
@@ -283,7 +293,7 @@ internal abstract class CacheWindowLogic(
283293
// If we get the same delta in the next frame, would we cover the extra space needed
284294
// to actually need this item? If so, mark it as urgent
285295
val isUrgent: Boolean =
286-
if (prefetchWindowEndLine + 1 == visibleWindowEnd + 1) {
296+
if (prefetchWindowEndLine + 1 == visibleWindowEnd + 1 && scrollDelta != 0.0f) {
287297
scrollDelta.absoluteValue >= mainAxisExtraSpaceEnd
288298
} else {
289299
false
@@ -315,7 +325,9 @@ internal abstract class CacheWindowLogic(
315325
// If we get the same delta in the next frame, would we cover the extra space needed
316326
// to actually need this item? If so, mark it as urgent
317327
val isUrgent: Boolean =
318-
if (prefetchWindowStartLine - 1 == visibleWindowStart - 1) {
328+
if (
329+
prefetchWindowStartLine - 1 == visibleWindowStart - 1 && scrollDelta != 0.0f
330+
) {
319331
scrollDelta.absoluteValue >= mainAxisExtraSpaceStart
320332
} else {
321333
false
@@ -358,7 +370,7 @@ internal abstract class CacheWindowLogic(
358370
while (prefetchWindowStartExtraSpace > 0 && prefetchWindowStartLine > 0) {
359371
val item =
360372
if (windowCache.containsKey(prefetchWindowStartLine - 1)) {
361-
windowCache[prefetchWindowStartLine - 1]
373+
windowCache[prefetchWindowStartLine - 1]!!.mainAxisSize
362374
} else {
363375
break
364376
}
@@ -373,7 +385,7 @@ internal abstract class CacheWindowLogic(
373385
while (prefetchWindowEndExtraSpace > 0 && prefetchWindowEndLine < itemsCount - 1) {
374386
val item =
375387
if (windowCache.containsKey(prefetchWindowEndLine + 1)) {
376-
windowCache[prefetchWindowEndLine + 1]
388+
windowCache[prefetchWindowEndLine + 1]!!.mainAxisSize
377389
} else {
378390
break
379391
}
@@ -386,13 +398,16 @@ internal abstract class CacheWindowLogic(
386398

387399
private fun CacheWindowScope.getItemSizeOrPrefetch(index: Int, isUrgent: Boolean): Int {
388400
return if (windowCache.containsKey(index)) {
389-
windowCache[index]
401+
debugLog { "Item $index is Cached!" }
402+
windowCache[index]!!.mainAxisSize
390403
} else if (prefetchWindowHandles.containsKey(index)) {
391404
// item is scheduled but didn't finish yet
405+
debugLog { "Item=$index is already scheduled. isUrgent=$isUrgent" }
392406
if (isUrgent) prefetchWindowHandles[index]?.fastForEach { it.markAsUrgent() }
393407
InvalidItemSize
394408
} else {
395409
// item is not scheduled
410+
debugLog { "Scheduling Prefetching for Item=$index. isUrgent=$isUrgent" }
396411
prefetchWindowHandles[index] =
397412
schedulePrefetch(index) { prefetchedIndex, size ->
398413
onItemPrefetched(prefetchedIndex, size)
@@ -404,7 +419,7 @@ internal abstract class CacheWindowLogic(
404419

405420
/** Grows the window with measured items and prefetched items. */
406421
private fun cachePrefetchedItem(index: Int, size: Int) {
407-
windowCache[index] = size
422+
windowCache[index] = updateOrCreateCachedItem(index, size, CachedItem.NoKey)
408423
if (index > prefetchWindowEndLine) {
409424
prefetchWindowEndLine = index
410425
prefetchWindowEndExtraSpace -= size
@@ -414,18 +429,34 @@ internal abstract class CacheWindowLogic(
414429
}
415430
}
416431

432+
private fun updateOrCreateCachedItem(index: Int, size: Int, key: Any): CachedItem {
433+
val cachedItem = windowCache[index]
434+
return if (cachedItem != null) {
435+
cachedItem.mainAxisSize = size
436+
cachedItem.key = key
437+
cachedItem
438+
} else {
439+
CachedItem(CachedItem.NoKey, size)
440+
}
441+
}
442+
417443
/**
418444
* When caching visible items we need to check if the existing item changed sizes. If so, we
419445
* will set [shouldRefillWindow] which will trigger a complete window filling and cancel any out
420-
* of bounds requests.
446+
* of bounds requests. The same is valid if items are replaced (have the same size by key
447+
* changed).
421448
*/
422-
private fun cacheVisibleItemsInfo(index: Int, size: Int) {
423-
debugLog { "cacheVisibleItemsInfo item=$index size=$size" }
424-
if (windowCache.containsKey(index) && windowCache[index] != size) {
425-
shouldRefillWindow = true
449+
private fun cacheVisibleItemsInfo(index: Int, key: Any, size: Int) {
450+
debugLog { "cacheVisibleItemsInfo item=$index size=$size key=$key" }
451+
if (windowCache.containsKey(index)) {
452+
val cachedSize = windowCache[index]!!.mainAxisSize
453+
val cachedKey = windowCache[index]!!.key
454+
if (cachedSize != size || cachedKey != key) {
455+
shouldRefillWindow = true
456+
}
426457
}
427458

428-
windowCache[index] = size
459+
windowCache[index] = updateOrCreateCachedItem(index, size, key)
429460
// We're caching a visible item, remove its handle since we won't need it anymore.
430461
prefetchWindowStartLine = minOf(prefetchWindowStartLine, index)
431462
prefetchWindowEndLine = maxOf(prefetchWindowEndLine, index)
@@ -439,6 +470,8 @@ internal abstract class CacheWindowLogic(
439470

440471
windowCache.forEachKey { if (it in startLine..endLine) indicesToRemove.add(it) }
441472

473+
debugLog { "Indices to remove=$indicesToRemove" }
474+
442475
indicesToRemove.forEach {
443476
prefetchWindowHandles.remove(it)?.fastForEach { it.cancel() }
444477
windowCache.remove(it)
@@ -501,15 +534,19 @@ internal interface CacheWindowScope {
501534

502535
fun getVisibleItemLine(indexInVisibleLines: Int): Int
503536

537+
fun getVisibleLineKey(indexInVisibleLines: Int): Any
538+
504539
fun getLastIndexInLine(lineIndex: Int): Int
505540

506541
fun getLastLineIndex(): Int
507542
}
508543

509544
internal inline fun CacheWindowScope.forEachVisibleItem(
510-
action: (itemIndex: Int, mainAxisSize: Int) -> Unit
545+
action: (itemIndex: Int, itemKey: Any, mainAxisSize: Int) -> Unit
511546
) {
512-
repeat(visibleLineCount) { action(getVisibleItemLine(it), getVisibleItemSize(it)) }
547+
repeat(visibleLineCount) {
548+
action(getVisibleItemLine(it), getVisibleLineKey(it), getVisibleItemSize(it))
549+
}
513550
}
514551

515552
private const val InvalidItemSize = -1
@@ -523,3 +560,7 @@ private inline fun debugLog(generateMsg: () -> String) {
523560
println("CacheWindowLogic: ${generateMsg()}")
524561
}
525562
}
563+
564+
internal class CachedItem(var key: Any, var mainAxisSize: Int) {
565+
companion object NoKey
566+
}

compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerCacheWindowLogic.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package androidx.compose.foundation.pager
1919
import androidx.compose.foundation.ExperimentalFoundationApi
2020
import androidx.compose.foundation.lazy.layout.CacheWindowLogic
2121
import androidx.compose.foundation.lazy.layout.CacheWindowScope
22+
import androidx.compose.foundation.lazy.layout.CachedItem
2223
import androidx.compose.foundation.lazy.layout.InvalidIndex
2324
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
2425
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
@@ -156,6 +157,30 @@ private class PagerCacheWindowScope(val itemCount: () -> Int) : CacheWindowScope
156157
return InvalidIndex
157158
}
158159

160+
override fun getVisibleLineKey(indexInVisibleLines: Int): Any {
161+
val extraPagesBeforeCount = layoutInfo.extraPagesBefore.size
162+
163+
val visiblePagesCount = layoutInfo.visiblePagesInfo.size
164+
165+
if (indexInVisibleLines < extraPagesBeforeCount) {
166+
return layoutInfo.extraPagesBefore[indexInVisibleLines].key
167+
}
168+
169+
if (
170+
indexInVisibleLines >= extraPagesBeforeCount &&
171+
indexInVisibleLines < extraPagesBeforeCount + visiblePagesCount
172+
) {
173+
return layoutInfo.visiblePagesInfo[indexInVisibleLines - extraPagesBeforeCount].key
174+
}
175+
176+
if (indexInVisibleLines >= extraPagesBeforeCount + visiblePagesCount) {
177+
return layoutInfo.extraPagesAfter[
178+
indexInVisibleLines - extraPagesBeforeCount - visiblePagesCount]
179+
.key
180+
}
181+
return CachedItem.NoKey
182+
}
183+
159184
override fun getLastIndexInLine(lineIndex: Int): Int = lineIndex
160185

161186
override fun getLastLineIndex(): Int {

0 commit comments

Comments
 (0)