1 /*
<lambda>null2 * Copyright 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.foundation.lazy.grid
18
19 import androidx.annotation.IntRange as AndroidXIntRange
20 import androidx.compose.foundation.ExperimentalFoundationApi
21 import androidx.compose.foundation.MutatePriority
22 import androidx.compose.foundation.gestures.Orientation
23 import androidx.compose.foundation.gestures.ScrollScope
24 import androidx.compose.foundation.gestures.ScrollableState
25 import androidx.compose.foundation.gestures.stopScroll
26 import androidx.compose.foundation.interaction.InteractionSource
27 import androidx.compose.foundation.interaction.MutableInteractionSource
28 import androidx.compose.foundation.internal.checkPrecondition
29 import androidx.compose.foundation.lazy.grid.LazyGridState.Companion.Saver
30 import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
31 import androidx.compose.foundation.lazy.layout.CacheWindowLogic
32 import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
33 import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
34 import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
35 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
36 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
37 import androidx.compose.foundation.lazy.layout.LazyLayoutScrollDeltaBetweenPasses
38 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
39 import androidx.compose.foundation.lazy.layout.animateScrollToItem
40 import androidx.compose.runtime.Composable
41 import androidx.compose.runtime.Stable
42 import androidx.compose.runtime.annotation.FrequentlyChangingValue
43 import androidx.compose.runtime.getValue
44 import androidx.compose.runtime.mutableStateOf
45 import androidx.compose.runtime.neverEqualPolicy
46 import androidx.compose.runtime.remember
47 import androidx.compose.runtime.saveable.Saver
48 import androidx.compose.runtime.saveable.listSaver
49 import androidx.compose.runtime.saveable.rememberSaveable
50 import androidx.compose.runtime.setValue
51 import androidx.compose.runtime.snapshots.Snapshot
52 import androidx.compose.ui.layout.AlignmentLine
53 import androidx.compose.ui.layout.MeasureResult
54 import androidx.compose.ui.layout.Remeasurement
55 import androidx.compose.ui.layout.RemeasurementModifier
56 import androidx.compose.ui.unit.Density
57 import androidx.compose.ui.util.fastForEach
58 import kotlin.coroutines.EmptyCoroutineContext
59 import kotlin.math.abs
60 import kotlin.math.roundToInt
61 import kotlinx.coroutines.CoroutineScope
62 import kotlinx.coroutines.launch
63
64 /**
65 * Creates a [LazyGridState] that is remembered across compositions.
66 *
67 * Changes to the provided initial values will **not** result in the state being recreated or
68 * changed in any way if it has already been created.
69 *
70 * @param initialFirstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
71 * @param initialFirstVisibleItemScrollOffset the initial value for
72 * [LazyGridState.firstVisibleItemScrollOffset]
73 */
74 @Composable
75 fun rememberLazyGridState(
76 initialFirstVisibleItemIndex: Int = 0,
77 initialFirstVisibleItemScrollOffset: Int = 0
78 ): LazyGridState {
79 return rememberSaveable(saver = LazyGridState.Saver) {
80 LazyGridState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset)
81 }
82 }
83
84 /**
85 * Creates a [LazyGridState] that is remembered across compositions.
86 *
87 * Changes to the provided initial values will **not** result in the state being recreated or
88 * changed in any way if it has already been created.
89 *
90 * @param initialFirstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
91 * @param initialFirstVisibleItemScrollOffset the initial value for
92 * [LazyGridState.firstVisibleItemScrollOffset]
93 * @param prefetchStrategy the [LazyGridPrefetchStrategy] to use for prefetching content in this
94 * grid
95 */
96 @ExperimentalFoundationApi
97 @Composable
rememberLazyGridStatenull98 fun rememberLazyGridState(
99 initialFirstVisibleItemIndex: Int = 0,
100 initialFirstVisibleItemScrollOffset: Int = 0,
101 prefetchStrategy: LazyGridPrefetchStrategy = remember { LazyGridPrefetchStrategy() },
102 ): LazyGridState {
<lambda>null103 return rememberSaveable(prefetchStrategy, saver = LazyGridState.saver(prefetchStrategy)) {
104 LazyGridState(
105 initialFirstVisibleItemIndex,
106 initialFirstVisibleItemScrollOffset,
107 prefetchStrategy,
108 )
109 }
110 }
111
112 /**
113 * Creates a [LazyGridState] that is remembered across compositions.
114 *
115 * Changes to the provided initial values will **not** result in the state being recreated or
116 * changed in any way if it has already been created.
117 *
118 * @param cacheWindow specifies the size of the ahead and behind window to be used as per
119 * [LazyLayoutCacheWindow].
120 * @param initialFirstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
121 * @param initialFirstVisibleItemScrollOffset the initial value for
122 * [LazyGridState.firstVisibleItemScrollOffset]
123 */
124 @ExperimentalFoundationApi
125 @Composable
rememberLazyGridStatenull126 fun rememberLazyGridState(
127 cacheWindow: LazyLayoutCacheWindow,
128 initialFirstVisibleItemIndex: Int = 0,
129 initialFirstVisibleItemScrollOffset: Int = 0
130 ): LazyGridState {
131 return rememberSaveable(cacheWindow, saver = LazyGridState.saver(cacheWindow)) {
132 LazyGridState(
133 cacheWindow,
134 initialFirstVisibleItemIndex,
135 initialFirstVisibleItemScrollOffset,
136 )
137 }
138 }
139
140 /**
141 * A state object that can be hoisted to control and observe scrolling.
142 *
143 * In most cases, this will be created via [rememberLazyGridState].
144 *
145 * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
146 * @param firstVisibleItemScrollOffset the initial value for
147 * [LazyGridState.firstVisibleItemScrollOffset]
148 * @param prefetchStrategy the [LazyGridPrefetchStrategy] to use for prefetching content in this
149 * grid
150 */
151 @OptIn(ExperimentalFoundationApi::class)
152 @Stable
153 class LazyGridState
154 @ExperimentalFoundationApi
155 constructor(
156 firstVisibleItemIndex: Int = 0,
157 firstVisibleItemScrollOffset: Int = 0,
158 internal val prefetchStrategy: LazyGridPrefetchStrategy = LazyGridPrefetchStrategy(),
159 ) : ScrollableState {
160
161 /**
162 * @param cacheWindow specifies the size of the ahead and behind window to be used as per
163 * [LazyLayoutCacheWindow].
164 * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
165 * @param firstVisibleItemScrollOffset the initial value for
166 * [LazyGridState.firstVisibleItemScrollOffset]
167 */
168 @ExperimentalFoundationApi
169 constructor(
170 cacheWindow: LazyLayoutCacheWindow,
171 firstVisibleItemIndex: Int = 0,
172 firstVisibleItemScrollOffset: Int = 0
173 ) : this(
174 firstVisibleItemIndex,
175 firstVisibleItemScrollOffset,
176 LazyGridCacheWindowPrefetchStrategy(cacheWindow)
177 )
178
179 /**
180 * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex]
181 * @param firstVisibleItemScrollOffset the initial value for
182 * [LazyGridState.firstVisibleItemScrollOffset]
183 */
184 constructor(
185 firstVisibleItemIndex: Int = 0,
186 firstVisibleItemScrollOffset: Int = 0
187 ) : this(firstVisibleItemIndex, firstVisibleItemScrollOffset, LazyGridPrefetchStrategy())
188
189 internal var hasLookaheadOccurred: Boolean = false
190 private set
191
192 internal var approachLayoutInfo: LazyGridMeasureResult? = null
193 private set
194
195 // always execute requests in high priority
196 private var executeRequestsInHighPriorityMode = false
197
198 /** The holder class for the current scroll position. */
199 private val scrollPosition =
200 LazyGridScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
201
202 /**
203 * The index of the first item that is visible within the scrollable viewport area, this means,
204 * not including items in the content padding region. For the first visible item that includes
205 * items in the content padding please use [LazyGridLayoutInfo.visibleItemsInfo].
206 *
207 * Note that this property is observable and if you use it in the composable function it will be
208 * recomposed on every change causing potential performance issues.
209 *
210 * If you want to run some side effects like sending an analytics event or updating a state
211 * based on this value consider using "snapshotFlow":
212 *
213 * @sample androidx.compose.foundation.samples.UsingGridScrollPositionForSideEffectSample
214 *
215 * If you need to use it in the composition then consider wrapping the calculation into a
216 * derived state in order to only have recompositions when the derived value changes:
217 *
218 * @sample androidx.compose.foundation.samples.UsingGridScrollPositionInCompositionSample
219 */
220 val firstVisibleItemIndex: Int
221 @FrequentlyChangingValue get() = scrollPosition.index
222
223 /**
224 * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the amount
225 * that the item is offset backwards
226 */
227 val firstVisibleItemScrollOffset: Int
228 @FrequentlyChangingValue get() = scrollPosition.scrollOffset
229
230 /** Backing state for [layoutInfo] */
231 private val layoutInfoState = mutableStateOf(EmptyLazyGridLayoutInfo, neverEqualPolicy())
232
233 /**
234 * The object of [LazyGridLayoutInfo] calculated during the last layout pass. For example, you
235 * can use it to calculate what items are currently visible.
236 *
237 * Note that this property is observable and is updated after every scroll or remeasure. If you
238 * use it in the composable function it will be recomposed on every change causing potential
239 * performance issues including infinity recomposition loop. Therefore, avoid using it in the
240 * composition.
241 *
242 * If you want to run some side effects like sending an analytics event or updating a state
243 * based on this value consider using "snapshotFlow":
244 *
245 * @sample androidx.compose.foundation.samples.UsingGridLayoutInfoForSideEffectSample
246 */
247 val layoutInfo: LazyGridLayoutInfo
248 @FrequentlyChangingValue get() = layoutInfoState.value
249
250 /**
251 * [InteractionSource] that will be used to dispatch drag events when this grid is being
252 * dragged. If you want to know whether the fling (or animated scroll) is in progress, use
253 * [isScrollInProgress].
254 */
255 val interactionSource: InteractionSource
256 get() = internalInteractionSource
257
258 internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
259
260 /**
261 * The amount of scroll to be consumed in the next layout pass. Scrolling forward is negative
262 * - that is, it is the amount that the items are offset in y
263 */
264 internal var scrollToBeConsumed = 0f
265 private set
266
267 internal val slotsPerLine: Int
268 get() = layoutInfoState.value.slotsPerLine
269
270 internal val density: Density
271 get() = layoutInfoState.value.density
272
273 /**
274 * The ScrollableController instance. We keep it as we need to call stopAnimation on it once we
275 * reached the end of the grid.
276 */
<lambda>null277 private val scrollableState = ScrollableState { -onScroll(-it) }
278
279 /** Only used for testing to confirm that we're not making too many measure passes */
280 /*@VisibleForTesting*/
281 internal var numMeasurePasses: Int = 0
282 private set
283
284 /** Only used for testing to disable prefetching when needed to test the main logic. */
285 /*@VisibleForTesting*/
286 internal var prefetchingEnabled: Boolean = true
287
288 /**
289 * The [Remeasurement] object associated with our layout. It allows us to remeasure
290 * synchronously during scroll.
291 */
292 internal var remeasurement: Remeasurement? = null
293 private set
294
295 /** The modifier which provides [remeasurement]. */
296 internal val remeasurementModifier =
297 object : RemeasurementModifier {
onRemeasurementAvailablenull298 override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
299 this@LazyGridState.remeasurement = remeasurement
300 }
301 }
302
303 /**
304 * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is
305 * ready.
306 */
307 internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
308
309 internal val itemAnimator = LazyLayoutItemAnimator<LazyGridMeasuredItem>()
310
311 internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
312
313 internal val prefetchState =
<lambda>null314 LazyLayoutPrefetchState(prefetchStrategy.prefetchScheduler) {
315 with(prefetchStrategy) {
316 onNestedPrefetch(Snapshot.withoutReadObservation { firstVisibleItemIndex })
317 }
318 }
319
320 private val prefetchScope: LazyGridPrefetchScope =
321 object : LazyGridPrefetchScope {
scheduleLinePrefetchnull322 override fun scheduleLinePrefetch(
323 lineIndex: Int
324 ): List<LazyLayoutPrefetchState.PrefetchHandle> {
325 return scheduleLinePrefetch(lineIndex, null)
326 }
327
328 @Suppress("PrimitiveInCollection")
scheduleLinePrefetchnull329 override fun scheduleLinePrefetch(
330 lineIndex: Int,
331 onPrefetchFinished: (LazyGridPrefetchResultScope.() -> Unit)?
332 ): List<LazyLayoutPrefetchState.PrefetchHandle> {
333 // Without read observation since this can be triggered from scroll - this will then
334 // cause us to recompose when the measure result changes. We don't care since the
335 // prefetch is best effort.
336 val prefetchHandles = mutableListOf<LazyLayoutPrefetchState.PrefetchHandle>()
337 val itemSizes: MutableList<Int>? =
338 if (onPrefetchFinished == null) null else mutableListOf()
339
340 Snapshot.withoutReadObservation {
341 val layoutInfo =
342 if (hasLookaheadOccurred) {
343 approachLayoutInfo
344 } else {
345 layoutInfoState.value
346 }
347
348 layoutInfo?.let { measureResult ->
349 var completedCount = 1
350 val itemsInLineInfo = measureResult.prefetchInfoRetriever(lineIndex)
351 itemsInLineInfo.fastForEach { lineInfo ->
352 prefetchHandles.add(
353 prefetchState.schedulePrecompositionAndPremeasure(
354 lineInfo.first,
355 lineInfo.second,
356 executeRequestsInHighPriorityMode
357 ) {
358 var itemMainAxisItemSize = 0
359 repeat(placeablesCount) {
360 itemMainAxisItemSize +=
361 if (measureResult.orientation == Orientation.Vertical) {
362 getSize(it).height
363 } else {
364 getSize(it).width
365 }
366 }
367
368 itemSizes?.add(itemMainAxisItemSize)
369 // all items in this line were prefetched, report the size
370 if (completedCount == itemsInLineInfo.size) {
371 if (onPrefetchFinished != null && itemSizes != null) {
372 onPrefetchFinished.invoke(
373 LazyGridPrefetchResultScopeImpl(
374 lineIndex,
375 itemSizes
376 )
377 )
378 }
379 } else {
380 completedCount++
381 }
382 }
383 )
384 }
385 }
386 }
387 return prefetchHandles
388 }
389 }
390
391 /** Stores currently pinned items which are always composed. */
392 internal val pinnedItems = LazyLayoutPinnedItemList()
393
394 internal val nearestRange: IntRange by scrollPosition.nearestRangeState
395
396 internal val placementScopeInvalidator = ObservableScopeInvalidator()
397
398 /**
399 * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
400 * pixels.
401 *
402 * @param index the index to which to scroll. Must be non-negative.
403 * @param scrollOffset the offset that the item should end up after the scroll. Note that
404 * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
405 * scroll the item further upward (taking it partly offscreen).
406 */
scrollToItemnull407 suspend fun scrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) {
408 scroll { snapToItemIndexInternal(index, scrollOffset, forceRemeasure = true) }
409 }
410
411 internal val measurementScopeInvalidator = ObservableScopeInvalidator()
412
413 /**
414 * Requests the item at [index] to be at the start of the viewport during the next remeasure,
415 * offset by [scrollOffset], and schedules a remeasure.
416 *
417 * The scroll position will be updated to the requested position rather than maintain the index
418 * based on the first visible item key (when a data set change will also be applied during the
419 * next remeasure), but *only* for the next remeasure.
420 *
421 * Any scroll in progress will be cancelled.
422 *
423 * @param index the index to which to scroll. Must be non-negative.
424 * @param scrollOffset the offset that the item should end up after the scroll. Note that
425 * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
426 * scroll the item further upward (taking it partly offscreen).
427 */
requestScrollToItemnull428 fun requestScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) {
429 // Cancel any scroll in progress.
430 if (isScrollInProgress) {
431 layoutInfoState.value.coroutineScope.launch { stopScroll() }
432 }
433
434 snapToItemIndexInternal(index, scrollOffset, forceRemeasure = false)
435 }
436
snapToItemIndexInternalnull437 internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int, forceRemeasure: Boolean) {
438 val positionChanged =
439 scrollPosition.index != index || scrollPosition.scrollOffset != scrollOffset
440 // sometimes this method is called not to scroll, but to stay on the same index when
441 // the data changes, as by default we maintain the scroll position by key, not index.
442 // when this happens we don't need to reset the animations as from the user perspective
443 // we didn't scroll anywhere and if there is an offset change for an item, this change
444 // should be animated.
445 // however, when the request is to really scroll to a different position, we have to
446 // reset previously known item positions as we don't want offset changes to be animated.
447 // this offset should be considered as a scroll, not the placement change.
448 if (positionChanged) {
449 itemAnimator.reset()
450 // we changed positions, cancel existing requests and wait for the next scroll to
451 // refill the window
452 (prefetchStrategy as? CacheWindowLogic)?.resetStrategy()
453 }
454 scrollPosition.requestPositionAndForgetLastKnownKey(index, scrollOffset)
455 if (forceRemeasure) {
456 remeasurement?.forceRemeasure()
457 } else {
458 measurementScopeInvalidator.invalidateScope()
459 }
460 }
461
462 /**
463 * Call this function to take control of scrolling and gain the ability to send scroll events
464 * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
465 * performed within a [scroll] block (even if they don't call any other methods on this object)
466 * in order to guarantee that mutual exclusion is enforced.
467 *
468 * If [scroll] is called from elsewhere, this will be canceled.
469 */
scrollnull470 override suspend fun scroll(
471 scrollPriority: MutatePriority,
472 block: suspend ScrollScope.() -> Unit
473 ) {
474 awaitLayoutModifier.waitForFirstLayout()
475 scrollableState.scroll(scrollPriority, block)
476 }
477
dispatchRawDeltanull478 override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta)
479
480 override val isScrollInProgress: Boolean
481 get() = scrollableState.isScrollInProgress
482
483 override var canScrollForward: Boolean by mutableStateOf(false)
484 private set
485
486 override var canScrollBackward: Boolean by mutableStateOf(false)
487 private set
488
489 @get:Suppress("GetterSetterNames")
490 override val lastScrolledForward: Boolean
491 get() = scrollableState.lastScrolledForward
492
493 @get:Suppress("GetterSetterNames")
494 override val lastScrolledBackward: Boolean
495 get() = scrollableState.lastScrolledBackward
496
497 // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
498 // fine-grained control over scrolling
499 /*@VisibleForTesting*/
500 internal fun onScroll(distance: Float): Float {
501 if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
502 return 0f
503 }
504 checkPrecondition(abs(scrollToBeConsumed) <= 0.5f) {
505 "entered drag with non-zero pending scroll"
506 }
507 scrollToBeConsumed += distance
508
509 // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
510 // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
511 // we have less than 0.5 pixels
512 if (abs(scrollToBeConsumed) > 0.5f) {
513 val preScrollToBeConsumed = scrollToBeConsumed
514 val intDelta = scrollToBeConsumed.roundToInt()
515 var scrolledLayoutInfo =
516 layoutInfoState.value.copyWithScrollDeltaWithoutRemeasure(
517 delta = intDelta,
518 updateAnimations = !hasLookaheadOccurred
519 )
520 if (scrolledLayoutInfo != null && this.approachLayoutInfo != null) {
521 // if we were able to scroll the lookahead layout info without remeasure, lets
522 // try to do the same for post lookahead layout info (sometimes they diverge).
523 val scrolledApproachLayoutInfo =
524 approachLayoutInfo?.copyWithScrollDeltaWithoutRemeasure(
525 delta = intDelta,
526 updateAnimations = true
527 )
528 if (scrolledApproachLayoutInfo != null) {
529 // we can apply scroll delta for both phases without remeasure
530 approachLayoutInfo = scrolledApproachLayoutInfo
531 } else {
532 // we can't apply scroll delta for post lookahead, so we have to remeasure
533 scrolledLayoutInfo = null
534 }
535 }
536 if (scrolledLayoutInfo != null) {
537 applyMeasureResult(
538 result = scrolledLayoutInfo,
539 isLookingAhead = hasLookaheadOccurred,
540 visibleItemsStayedTheSame = true
541 )
542 // we don't need to remeasure, so we only trigger re-placement:
543 placementScopeInvalidator.invalidateScope()
544
545 notifyPrefetchOnScroll(
546 preScrollToBeConsumed - scrollToBeConsumed,
547 scrolledLayoutInfo
548 )
549 } else {
550 remeasurement?.forceRemeasure()
551 notifyPrefetchOnScroll(preScrollToBeConsumed - scrollToBeConsumed, this.layoutInfo)
552 }
553 }
554
555 // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
556 if (abs(scrollToBeConsumed) <= 0.5f) {
557 // We consumed all of it - we'll hold onto the fractional scroll for later, so report
558 // that we consumed the whole thing
559 return distance
560 } else {
561 val scrollConsumed = distance - scrollToBeConsumed
562 // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
563 // nested scrolling)
564 scrollToBeConsumed = 0f // We're not consuming the rest, give it back
565 return scrollConsumed
566 }
567 }
568
notifyPrefetchOnScrollnull569 private fun notifyPrefetchOnScroll(delta: Float, layoutInfo: LazyGridLayoutInfo) {
570 if (prefetchingEnabled) {
571 with(prefetchStrategy) { prefetchScope.onScroll(delta, layoutInfo) }
572 }
573 }
574
575 private val numOfItemsToTeleport: Int
576 get() = 100 * slotsPerLine
577
578 /**
579 * Animate (smooth scroll) to the given item.
580 *
581 * @param index the index to which to scroll. Must be non-negative.
582 * @param scrollOffset the offset that the item should end up after the scroll. Note that
583 * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
584 * scroll the item further upward (taking it partly offscreen).
585 */
animateScrollToItemnull586 suspend fun animateScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) {
587 scroll {
588 LazyLayoutScrollScope(this@LazyGridState, this)
589 .animateScrollToItem(index, scrollOffset, numOfItemsToTeleport, density)
590 }
591 }
592
593 /** Updates the state with the new calculated scroll position and consumed scroll. */
applyMeasureResultnull594 internal fun applyMeasureResult(
595 result: LazyGridMeasureResult,
596 isLookingAhead: Boolean,
597 visibleItemsStayedTheSame: Boolean = false
598 ) {
599 // update the prefetch state with the number of nested prefetch items this layout
600 // should use.
601 prefetchState.idealNestedPrefetchCount = result.visibleItemsInfo.size
602
603 if (!isLookingAhead && hasLookaheadOccurred) {
604 // If there was already a lookahead pass, record this result as Approach result
605 approachLayoutInfo = result
606 } else {
607 if (isLookingAhead) {
608 hasLookaheadOccurred = true
609 }
610 scrollToBeConsumed -= result.consumedScroll
611 layoutInfoState.value = result
612
613 canScrollBackward = result.canScrollBackward
614 canScrollForward = result.canScrollForward
615
616 if (visibleItemsStayedTheSame) {
617 scrollPosition.updateScrollOffset(result.firstVisibleLineScrollOffset)
618 } else {
619 scrollPosition.updateFromMeasureResult(result)
620 if (prefetchingEnabled) {
621 with(prefetchStrategy) { prefetchScope.onVisibleItemsUpdated(result) }
622 }
623 }
624
625 if (isLookingAhead) {
626 _lazyLayoutScrollDeltaBetweenPasses.updateScrollDeltaForApproach(
627 result.scrollBackAmount,
628 result.density,
629 result.coroutineScope
630 )
631 }
632 numMeasurePasses++
633 }
634 }
635
636 private val _lazyLayoutScrollDeltaBetweenPasses = LazyLayoutScrollDeltaBetweenPasses()
637
638 internal val scrollDeltaBetweenPasses
639 get() = _lazyLayoutScrollDeltaBetweenPasses.scrollDeltaBetweenPasses
640
641 /**
642 * When the user provided custom keys for the items we can try to detect when there were items
643 * added or removed before our current first visible item and keep this item as the first
644 * visible one even given that its index has been changed.
645 */
updateScrollPositionIfTheFirstItemWasMovednull646 internal fun updateScrollPositionIfTheFirstItemWasMoved(
647 itemProvider: LazyGridItemProvider,
648 firstItemIndex: Int
649 ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex)
650
651 companion object {
652 /** The default [Saver] implementation for [LazyGridState]. */
653 val Saver: Saver<LazyGridState, *> =
654 listSaver(
655 save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
656 restore = {
657 LazyGridState(
658 firstVisibleItemIndex = it[0],
659 firstVisibleItemScrollOffset = it[1]
660 )
661 }
662 )
663
664 /**
665 * A [Saver] implementation for [LazyGridState] that handles setting a custom
666 * [LazyGridPrefetchStrategy].
667 */
668 @ExperimentalFoundationApi
669 internal fun saver(prefetchStrategy: LazyGridPrefetchStrategy): Saver<LazyGridState, *> =
670 listSaver(
671 save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
672 restore = {
673 LazyGridState(
674 firstVisibleItemIndex = it[0],
675 firstVisibleItemScrollOffset = it[1],
676 prefetchStrategy
677 )
678 }
679 )
680
681 /**
682 * A [Saver] implementation for [LazyGridState] that handles setting a custom
683 * [LazyLayoutCacheWindow].
684 */
685 @ExperimentalFoundationApi
686 internal fun saver(cacheWindow: LazyLayoutCacheWindow): Saver<LazyGridState, *> =
687 listSaver(
688 save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
689 restore = {
690 LazyGridState(
691 cacheWindow = cacheWindow,
692 firstVisibleItemIndex = it[0],
693 firstVisibleItemScrollOffset = it[1],
694 )
695 }
696 )
697 }
698 }
699
700 private val EmptyLazyGridLayoutInfo =
701 LazyGridMeasureResult(
702 firstVisibleLine = null,
703 firstVisibleLineScrollOffset = 0,
704 canScrollForward = false,
705 consumedScroll = 0f,
706 measureResult =
707 object : MeasureResult {
708 override val width: Int = 0
709 override val height: Int = 0
710
711 @Suppress("PrimitiveInCollection")
712 override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
713
placeChildrennull714 override fun placeChildren() {}
715 },
716 scrollBackAmount = 0f,
717 visibleItemsInfo = emptyList(),
718 viewportStartOffset = 0,
719 viewportEndOffset = 0,
720 totalItemsCount = 0,
721 reverseLayout = false,
722 orientation = Orientation.Vertical,
723 afterContentPadding = 0,
724 mainAxisItemSpacing = 0,
725 remeasureNeeded = false,
726 density = Density(1f),
727 slotsPerLine = 0,
728 coroutineScope = CoroutineScope(EmptyCoroutineContext),
<lambda>null729 prefetchInfoRetriever = { emptyList() }
730 )
731