[go: nahoru, domu]

blob: 0d773c2ac298493e6a63c9bebf100fcf432a62ed [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
package androidx.compose.foundation.lazy.list
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.lazy.DefaultLazyListPrefetchStrategy
import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyListPrefetchScope
import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
import androidx.compose.foundation.lazy.layout.NestedPrefetchScope
import androidx.compose.foundation.lazy.layout.PrefetchScheduler
import androidx.compose.foundation.lazy.layout.TestPrefetchScheduler
import androidx.compose.foundation.lazy.list.LazyListPrefetchStrategyTest.RecordingLazyListPrefetchStrategy.Callback
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@LargeTest
@RunWith(Parameterized::class)
@OptIn(ExperimentalFoundationApi::class)
class LazyListPrefetchStrategyTest(
val config: Config
) : BaseLazyListTestWithOrientation(config.orientation) {
companion object {
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun initParameters(): Array<Any> = arrayOf(
Config(Orientation.Vertical),
Config(Orientation.Horizontal),
)
class Config(
val orientation: Orientation,
) {
override fun toString() = "orientation=$orientation"
}
private val LazyListLayoutInfo.visibleIndices: List<Int>
get() = visibleItemsInfo.map { it.index }.sorted()
}
private val itemsSizePx = 30
private val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
lateinit var state: LazyListState
private val scheduler = TestPrefetchScheduler()
@Test
fun callbacksTriggered_whenScrollForwardsWithoutVisibleItemsChanged() {
val strategy = RecordingLazyListPrefetchStrategy(scheduler)
composeList(prefetchStrategy = strategy)
assertThat(strategy.callbacks).containsExactly(
Callback.OnVisibleItemsUpdated(
visibleIndices = listOf(0, 1)
),
).inOrder()
strategy.reset()
rule.runOnIdle {
runBlocking {
state.scrollBy(5f)
}
}
assertThat(strategy.callbacks).containsExactly(
Callback.OnScroll(
delta = -5f,
visibleIndices = listOf(0, 1)
),
).inOrder()
}
@Test
fun callbacksTriggered_whenScrollBackwardsWithoutVisibleItemsChanged() {
val strategy = RecordingLazyListPrefetchStrategy(scheduler)
composeList(firstItem = 10, itemOffset = 10, prefetchStrategy = strategy)
assertThat(strategy.callbacks).containsExactly(
Callback.OnVisibleItemsUpdated(
visibleIndices = listOf(10, 11)
),
).inOrder()
strategy.reset()
rule.runOnIdle {
runBlocking {
state.scrollBy(-5f)
}
}
assertThat(strategy.callbacks).containsExactly(
Callback.OnScroll(
delta = 5f,
visibleIndices = listOf(10, 11)
),
).inOrder()
}
@Test
fun callbacksTriggered_whenScrollWithVisibleItemsChanged() {
val strategy = RecordingLazyListPrefetchStrategy(scheduler)
composeList(prefetchStrategy = strategy)
assertThat(strategy.callbacks).containsExactly(
Callback.OnVisibleItemsUpdated(
visibleIndices = listOf(0, 1)
),
).inOrder()
strategy.reset()
rule.runOnIdle {
runBlocking {
state.scrollBy(itemsSizePx + 5f)
}
}
assertThat(strategy.callbacks).containsExactly(
Callback.OnVisibleItemsUpdated(
visibleIndices = listOf(1, 2)
),
Callback.OnScroll(
delta = -(itemsSizePx + 5f),
visibleIndices = listOf(1, 2)
),
).inOrder()
}
@Test
fun callbacksTriggered_whenItemsChangedWithoutScroll() {
val strategy = RecordingLazyListPrefetchStrategy(scheduler)
val numItems = mutableStateOf(100)
composeList(prefetchStrategy = strategy, numItems = numItems)
assertThat(strategy.callbacks).containsExactly(
Callback.OnVisibleItemsUpdated(
visibleIndices = listOf(0, 1)
),
).inOrder()
strategy.reset()
numItems.value = 1
rule.waitForIdle()
assertThat(strategy.callbacks).containsExactly(
Callback.OnVisibleItemsUpdated(
visibleIndices = listOf(0)
),
).inOrder()
}
@Test
fun itemComposed_whenPrefetchedFromCallback() {
val strategy = PrefetchNextLargestIndexStrategy()
composeList(prefetchStrategy = strategy)
rule.runOnIdle {
runBlocking {
state.scrollBy(5f)
}
}
waitForPrefetch()
rule.onNodeWithTag("2")
.assertExists()
}
private fun waitForPrefetch() {
rule.runOnIdle {
scheduler.executeActiveRequests()
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun composeList(
firstItem: Int = 0,
itemOffset: Int = 0,
numItems: MutableState<Int> = mutableStateOf(100),
prefetchStrategy: LazyListPrefetchStrategy = DefaultLazyListPrefetchStrategy()
) {
rule.setContent {
state = rememberLazyListState(
initialFirstVisibleItemIndex = firstItem,
initialFirstVisibleItemScrollOffset = itemOffset,
prefetchStrategy = prefetchStrategy
)
LazyColumnOrRow(
Modifier.mainAxisSize(itemsSizeDp * 1.5f),
state,
) {
items(numItems.value) {
Spacer(
Modifier
.mainAxisSize(itemsSizeDp)
.fillMaxCrossAxis()
.testTag("$it")
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
)
}
}
}
}
/**
* LazyListPrefetchStrategy that just records callbacks without scheduling prefetches.
*/
private class RecordingLazyListPrefetchStrategy(
override val prefetchScheduler: PrefetchScheduler?
) : LazyListPrefetchStrategy {
sealed interface Callback {
data class OnScroll(val delta: Float, val visibleIndices: List<Int>) : Callback
data class OnVisibleItemsUpdated(val visibleIndices: List<Int>) : Callback
}
private val _callbacks: MutableList<Callback> = mutableListOf()
val callbacks: List<Callback> = _callbacks
override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
_callbacks.add(Callback.OnScroll(delta, layoutInfo.visibleIndices))
}
override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) {
_callbacks.add(Callback.OnVisibleItemsUpdated(layoutInfo.visibleIndices))
}
override fun NestedPrefetchScope.onNestedPrefetch(firstVisibleItemIndex: Int) = Unit
fun reset() {
_callbacks.clear()
}
}
/**
* LazyListPrefetchStrategy that always prefetches the next largest index off screen no matter
* the scroll direction.
*/
private class PrefetchNextLargestIndexStrategy : LazyListPrefetchStrategy {
private var handle: LazyLayoutPrefetchState.PrefetchHandle? = null
private var prefetchIndex: Int = -1
override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
val index = layoutInfo.visibleIndices.last() + 1
if (handle != null && index != prefetchIndex) {
cancelPrefetch()
}
handle = schedulePrefetch(index)
prefetchIndex = index
}
override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) =
Unit
override fun NestedPrefetchScope.onNestedPrefetch(firstVisibleItemIndex: Int) = Unit
private fun cancelPrefetch() {
handle?.cancel()
prefetchIndex = -1
}
}
}