blob: a3eeb886b05a3e105ea78bfb0d6783e4ddf43a46 [file] [log] [blame]
/*
* Copyright 2021 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.
*/
package androidx.compose.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.GridItemSpan
import androidx.compose.foundation.lazy.LazyGridItemSpanScope
import androidx.compose.foundation.lazy.LazyGridScope
import androidx.compose.foundation.lazy.LazyGridState
import androidx.compose.foundation.lazy.getDefaultLazyKeyFor
import androidx.compose.foundation.lazy.layout.IntervalHolder
import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.intervalForIndex
import androidx.compose.foundation.lazy.layout.intervalIndexForItemIndex
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collect
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun rememberStateOfItemsProvider(
state: LazyGridState,
content: LazyGridScope.() -> Unit
): State<LazyGridItemsProvider> {
val latestContent = rememberUpdatedState(content)
val nearestItemsRangeState = remember(state) {
mutableStateOf(
calculateNearestItemsRange(state.firstVisibleItemIndexNonObservable.value)
)
}
LaunchedEffect(nearestItemsRangeState) {
snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
// MutableState's SnapshotMutationPolicy will make sure the provider is only
// recreated when the state is updated with a new range.
.collect { nearestItemsRangeState.value = it }
}
return remember(nearestItemsRangeState) {
derivedStateOf<LazyGridItemsProvider> {
val listScope = LazyGridScopeImpl().apply(latestContent.value)
LazyGridItemsProviderImpl(
listScope.intervals,
listScope.hasCustomSpans,
nearestItemsRangeState.value
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
internal class LazyGridItemsProviderImpl(
private val intervals: IntervalList<LazyGridIntervalContent>,
override val hasCustomSpans: Boolean,
nearestItemsRange: IntRange
) : LazyGridItemsProvider {
/**
* Caches the last interval we binary searched for. We might not need to recalculate
* for subsequent queries, as they tend to be localised.
*/
private var lastInterval: IntervalHolder<LazyGridIntervalContent>? = null
override val itemsCount get() = intervals.totalSize
override fun getKey(index: Int): Any {
val interval = getIntervalForIndex(index)
val localIntervalIndex = index - interval.startIndex
val key = interval.content.key?.invoke(localIntervalIndex)
return key ?: getDefaultLazyKeyFor(index)
}
override fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan {
val interval = getIntervalForIndex(index)
val localIntervalIndex = index - interval.startIndex
return interval.content.span.invoke(this, localIntervalIndex)
}
override fun getContent(index: Int): @Composable () -> Unit {
val interval = getIntervalForIndex(index)
val localIntervalIndex = index - interval.startIndex
return interval.content.content.invoke(localIntervalIndex)
}
override val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
// TODO(b/215372836) add content types support for grids.
override fun getContentType(index: Int): Any? = null
private fun getIntervalForIndex(itemIndex: Int) = lastInterval.let {
if (it != null && itemIndex in it.startIndex until it.startIndex + it.size) {
it
} else {
intervals.intervalForIndex(itemIndex).also { lastInterval = it }
}
}
}
/**
* Traverses the interval [list] in order to create a mapping from the key to the index for all
* the indexes in the passed [range].
* The returned map will not contain the values for intervals with no key mapping provided.
*/
internal fun generateKeyToIndexMap(
range: IntRange,
list: IntervalList<LazyGridIntervalContent>
): Map<Any, Int> {
val first = range.first
check(first >= 0)
val last = minOf(range.last, list.totalSize - 1)
return if (last < first) {
emptyMap()
} else {
hashMapOf<Any, Int>().also { map ->
var intervalIndex = list.intervalIndexForItemIndex(first)
var itemIndex = first
while (itemIndex <= last) {
val interval = list.intervals[intervalIndex]
val keyFactory = interval.content.key
if (keyFactory != null) {
val localItemIndex = itemIndex - interval.startIndex
if (localItemIndex == interval.size) {
intervalIndex++
} else {
map[keyFactory(localItemIndex)] = itemIndex
itemIndex++
}
} else {
intervalIndex++
itemIndex = interval.startIndex + interval.size
}
}
}
}
}
/**
* Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
* the first visible item. It is optimized to return the same range for small changes in the
* firstVisibleItem value so we do not regenerate the map on each scroll.
*/
private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
val slidingWindowStart = VisibleItemsSlidingWindowSize *
(firstVisibleItem / VisibleItemsSlidingWindowSize)
val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
return start until end
}
/**
* We use the idea of sliding window as an optimization, so user can scroll up to this number of
* items until we have to regenerate the key to index map.
*/
private val VisibleItemsSlidingWindowSize = 90
/**
* The minimum amount of items near the current first visible item we want to have mapping for.
*/
private val ExtraItemsNearTheSlidingWindow = 200