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.staggeredgrid 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.internal.requirePrecondition 30 import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier 31 import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo 32 import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator 33 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider 34 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList 35 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState 36 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle 37 import androidx.compose.foundation.lazy.layout.LazyLayoutScrollDeltaBetweenPasses 38 import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator 39 import androidx.compose.foundation.lazy.layout.PrefetchScheduler 40 import androidx.compose.foundation.lazy.layout.animateScrollToItem 41 import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.FullSpan 42 import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.Unset 43 import androidx.compose.runtime.Composable 44 import androidx.compose.runtime.Stable 45 import androidx.compose.runtime.getValue 46 import androidx.compose.runtime.mutableStateOf 47 import androidx.compose.runtime.neverEqualPolicy 48 import androidx.compose.runtime.saveable.listSaver 49 import androidx.compose.runtime.saveable.rememberSaveable 50 import androidx.compose.runtime.setValue 51 import androidx.compose.ui.layout.Remeasurement 52 import androidx.compose.ui.layout.RemeasurementModifier 53 import androidx.compose.ui.unit.Constraints 54 import kotlin.math.abs 55 import kotlin.math.roundToInt 56 import kotlin.ranges.IntRange 57 import kotlinx.coroutines.launch 58 59 /** 60 * Creates a [LazyStaggeredGridState] that is remembered across composition. 61 * 62 * Calling this function with different parameters on recomposition WILL NOT recreate or change the 63 * state. Use [LazyStaggeredGridState.scrollToItem] or [LazyStaggeredGridState.animateScrollToItem] 64 * to adjust position instead. 65 * 66 * @param initialFirstVisibleItemIndex initial position for 67 * [LazyStaggeredGridState.firstVisibleItemIndex] 68 * @param initialFirstVisibleItemScrollOffset initial value for 69 * [LazyStaggeredGridState.firstVisibleItemScrollOffset] 70 * @return created and memoized [LazyStaggeredGridState] with given parameters. 71 */ 72 @Composable 73 fun rememberLazyStaggeredGridState( 74 initialFirstVisibleItemIndex: Int = 0, 75 initialFirstVisibleItemScrollOffset: Int = 0 76 ): LazyStaggeredGridState = 77 rememberSaveable(saver = LazyStaggeredGridState.Saver) { 78 LazyStaggeredGridState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset) 79 } 80 81 /** 82 * Hoisted state object controlling [LazyVerticalStaggeredGrid] or [LazyHorizontalStaggeredGrid]. In 83 * most cases, it should be created via [rememberLazyStaggeredGridState]. 84 */ 85 @OptIn(ExperimentalFoundationApi::class) 86 @Stable 87 class LazyStaggeredGridState 88 internal constructor( 89 initialFirstVisibleItems: IntArray, 90 initialFirstVisibleOffsets: IntArray, 91 prefetchScheduler: PrefetchScheduler? 92 ) : ScrollableState { 93 /** 94 * @param initialFirstVisibleItemIndex initial value for [firstVisibleItemIndex] 95 * @param initialFirstVisibleItemOffset initial value for [firstVisibleItemScrollOffset] 96 */ 97 constructor( 98 initialFirstVisibleItemIndex: Int = 0, 99 initialFirstVisibleItemOffset: Int = 0 100 ) : this( 101 intArrayOf(initialFirstVisibleItemIndex), 102 intArrayOf(initialFirstVisibleItemOffset), 103 null 104 ) 105 106 internal var hasLookaheadOccurred: Boolean = false 107 private set 108 109 internal var approachLayoutInfo: LazyStaggeredGridMeasureResult? = null 110 private set 111 112 /** 113 * Index of the first visible item across all staggered grid lanes. This does not include items 114 * in the content padding region. For the first visible item that includes items in the content 115 * padding please use [LazyStaggeredGridLayoutInfo.visibleItemsInfo]. 116 * 117 * This property is observable and when use it in composable function it will be recomposed on 118 * each scroll, potentially causing performance issues. 119 */ 120 val firstVisibleItemIndex: Int 121 get() = scrollPosition.index 122 123 /** 124 * Current offset of the item with [firstVisibleItemIndex] relative to the container start. 125 * 126 * This property is observable and when use it in composable function it will be recomposed on 127 * each scroll, potentially causing performance issues. 128 */ 129 val firstVisibleItemScrollOffset: Int 130 get() = scrollPosition.scrollOffset 131 132 /** holder for current scroll position */ 133 internal val scrollPosition = 134 LazyStaggeredGridScrollPosition( 135 initialFirstVisibleItems, 136 initialFirstVisibleOffsets, 137 ::fillNearestIndices 138 ) 139 140 /** 141 * Layout information calculated during last layout pass, with information about currently 142 * visible items and container parameters. 143 * 144 * This property is observable and when use it in composable function it will be recomposed on 145 * each scroll, potentially causing performance issues. 146 */ 147 val layoutInfo: LazyStaggeredGridLayoutInfo 148 get() = layoutInfoState.value 149 150 /** backing state for [layoutInfo] */ 151 private val layoutInfoState = 152 mutableStateOf(EmptyLazyStaggeredGridLayoutInfo, neverEqualPolicy()) 153 154 /** storage for lane assignments for each item for consistent scrolling in both directions */ 155 internal val laneInfo = LazyStaggeredGridLaneInfo() 156 157 override var canScrollForward: Boolean by mutableStateOf(false) 158 private set 159 160 override var canScrollBackward: Boolean by mutableStateOf(false) 161 private set 162 163 @get:Suppress("GetterSetterNames") 164 override val lastScrolledForward: Boolean 165 get() = scrollableState.lastScrolledForward 166 167 @get:Suppress("GetterSetterNames") 168 override val lastScrolledBackward: Boolean 169 get() = scrollableState.lastScrolledBackward 170 171 internal var remeasurement: Remeasurement? = null 172 private set 173 174 internal val remeasurementModifier = 175 object : RemeasurementModifier { onRemeasurementAvailablenull176 override fun onRemeasurementAvailable(remeasurement: Remeasurement) { 177 this@LazyStaggeredGridState.remeasurement = remeasurement 178 } 179 } 180 181 /** 182 * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is 183 * ready. 184 */ 185 internal val awaitLayoutModifier = AwaitFirstLayoutModifier() 186 187 internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo() 188 189 /** Only used for testing to disable prefetching when needed to test the main logic. */ 190 /*@VisibleForTesting*/ 191 internal var prefetchingEnabled: Boolean = true 192 193 /** prefetch state used for precomputing items in the direction of scroll */ 194 internal val prefetchState: LazyLayoutPrefetchState = LazyLayoutPrefetchState(prefetchScheduler) 195 196 /** state controlling the scroll */ <lambda>null197 private val scrollableState = ScrollableState { -onScroll(-it) } 198 199 /** scroll to be consumed during next/current layout pass */ 200 private var scrollToBeConsumed = 0f 201 scrollToBeConsumednull202 internal fun scrollToBeConsumed(isLookingAhead: Boolean): Float = 203 if (isLookingAhead || !hasLookaheadOccurred) { 204 scrollToBeConsumed 205 } else { 206 scrollDeltaBetweenPasses 207 } 208 209 /* @VisibleForTesting */ 210 internal var measurePassCount = 0 211 212 /** prefetch state */ 213 private var prefetchBaseIndex: Int = -1 214 private val currentItemPrefetchHandles = mutableMapOf<Int, PrefetchHandle>() 215 216 internal val laneCount 217 get() = layoutInfoState.value.slots.sizes.size 218 219 /** 220 * [InteractionSource] that will be used to dispatch drag events when this list is being 221 * dragged. If you want to know whether the fling (or animated scroll) is in progress, use 222 * [isScrollInProgress]. 223 */ 224 val interactionSource 225 get(): InteractionSource = mutableInteractionSource 226 227 /** backing field mutable field for [interactionSource] */ 228 internal val mutableInteractionSource = MutableInteractionSource() 229 230 /** Stores currently pinned items which are always composed. */ 231 internal val pinnedItems = LazyLayoutPinnedItemList() 232 233 internal val itemAnimator = LazyLayoutItemAnimator<LazyStaggeredGridMeasuredItem>() 234 235 internal val nearestRange: IntRange by scrollPosition.nearestRangeState 236 237 internal val placementScopeInvalidator = ObservableScopeInvalidator() 238 239 /** 240 * Call this function to take control of scrolling and gain the ability to send scroll events 241 * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be 242 * performed within a [scroll] block (even if they don't call any other methods on this object) 243 * in order to guarantee that mutual exclusion is enforced. 244 * 245 * If [scroll] is called from elsewhere, this will be canceled. 246 */ scrollnull247 override suspend fun scroll( 248 scrollPriority: MutatePriority, 249 block: suspend ScrollScope.() -> Unit 250 ) { 251 awaitLayoutModifier.waitForFirstLayout() 252 scrollableState.scroll(scrollPriority, block) 253 } 254 255 /** 256 * Whether this [scrollableState] is currently scrolling by gesture, fling or programmatically 257 * or not. 258 */ 259 override val isScrollInProgress: Boolean 260 get() = scrollableState.isScrollInProgress 261 262 /** Main scroll callback which adjusts scroll delta and remeasures layout */ onScrollnull263 private fun onScroll(distance: Float): Float { 264 if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) { 265 return 0f 266 } 267 checkPrecondition(abs(scrollToBeConsumed) <= 0.5f) { 268 "entered drag with non-zero pending scroll" 269 } 270 scrollToBeConsumed += distance 271 272 // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation 273 // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if 274 // we have less than 0.5 pixels 275 if (abs(scrollToBeConsumed) > 0.5f) { 276 val preScrollToBeConsumed = scrollToBeConsumed 277 val intDelta = scrollToBeConsumed.roundToInt() 278 var scrolledLayoutInfo = 279 layoutInfoState.value.copyWithScrollDeltaWithoutRemeasure( 280 delta = intDelta, 281 updateAnimations = !hasLookaheadOccurred 282 ) 283 if (scrolledLayoutInfo != null && this.approachLayoutInfo != null) { 284 // if we were able to scroll the lookahead layout info without remeasure, lets 285 // try to do the same for post lookahead layout info (sometimes they diverge). 286 val scrolledApproachLayoutInfo = 287 approachLayoutInfo?.copyWithScrollDeltaWithoutRemeasure( 288 delta = intDelta, 289 updateAnimations = true 290 ) 291 if (scrolledApproachLayoutInfo != null) { 292 // we can apply scroll delta for both phases without remeasure 293 approachLayoutInfo = scrolledApproachLayoutInfo 294 } else { 295 // we can't apply scroll delta for post lookahead, so we have to remeasure 296 scrolledLayoutInfo = null 297 } 298 } 299 if (scrolledLayoutInfo != null) { 300 applyMeasureResult( 301 result = scrolledLayoutInfo, 302 isLookingAhead = hasLookaheadOccurred, 303 visibleItemsStayedTheSame = true 304 ) 305 // we don't need to remeasure, so we only trigger re-placement: 306 placementScopeInvalidator.invalidateScope() 307 308 notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed, scrolledLayoutInfo) 309 } else { 310 remeasurement?.forceRemeasure() 311 312 notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed) 313 } 314 } 315 316 // here scrollToBeConsumed is already consumed during the forceRemeasure invocation 317 if (abs(scrollToBeConsumed) <= 0.5f) { 318 // We consumed all of it - we'll hold onto the fractional scroll for later, so report 319 // that we consumed the whole thing 320 return distance 321 } else { 322 val scrollConsumed = distance - scrollToBeConsumed 323 // We did not consume all of it - return the rest to be consumed elsewhere (e.g., 324 // nested scrolling) 325 scrollToBeConsumed = 0f // We're not consuming the rest, give it back 326 return scrollConsumed 327 } 328 } 329 330 /** 331 * Instantly brings the item at [index] to the top of layout viewport, offset by [scrollOffset] 332 * pixels. 333 * 334 * @param index the index to which to scroll. MUST NOT be negative. 335 * @param scrollOffset the offset where the item should end up after the scroll. Note that 336 * positive offset refers to forward scroll, so in a reversed list, positive offset will 337 * scroll the item further upward (taking it partly offscreen). 338 */ scrollToItemnull339 suspend fun scrollToItem( 340 /* @IntRange(from = 0) */ 341 index: Int, 342 scrollOffset: Int = 0 343 ) { 344 scroll { snapToItemInternal(index, scrollOffset, forceRemeasure = true) } 345 } 346 347 /** 348 * Animate (smooth scroll) to the given item. 349 * 350 * @param index the index to which to scroll. MUST NOT be negative. 351 * @param scrollOffset the offset that the item should end up after the scroll. Note that 352 * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will 353 * scroll the item further upward (taking it partly offscreen). 354 */ animateScrollToItemnull355 suspend fun animateScrollToItem( 356 /* @IntRange(from = 0) */ 357 index: Int, 358 scrollOffset: Int = 0 359 ) { 360 val layoutInfo = layoutInfoState.value 361 val numOfItemsToTeleport = 100 * layoutInfo.slots.sizes.size 362 scroll { 363 LazyLayoutScrollScope(this@LazyStaggeredGridState, this) 364 .animateScrollToItem(index, scrollOffset, numOfItemsToTeleport, layoutInfo.density) 365 } 366 } 367 368 internal val measurementScopeInvalidator = ObservableScopeInvalidator() 369 370 /** 371 * Requests the item at [index] to be at the start of the viewport during the next remeasure, 372 * offset by [scrollOffset], and schedules a remeasure. 373 * 374 * The scroll position will be updated to the requested position rather than maintain the index 375 * based on the first visible item key (when a data set change will also be applied during the 376 * next remeasure), but *only* for the next remeasure. 377 * 378 * Any scroll in progress will be cancelled. 379 * 380 * @param index the index to which to scroll. Must be non-negative. 381 * @param scrollOffset the offset that the item should end up after the scroll. Note that 382 * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will 383 * scroll the item further upward (taking it partly offscreen). 384 */ requestScrollToItemnull385 fun requestScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) { 386 // Cancel any scroll in progress. 387 if (isScrollInProgress) { 388 layoutInfoState.value.coroutineScope.launch { stopScroll() } 389 } 390 391 snapToItemInternal(index, scrollOffset, forceRemeasure = false) 392 } 393 snapToItemInternalnull394 internal fun snapToItemInternal(index: Int, scrollOffset: Int, forceRemeasure: Boolean) { 395 val positionChanged = 396 scrollPosition.index != index || scrollPosition.scrollOffset != scrollOffset 397 // sometimes this method is called not to scroll, but to stay on the same index when 398 // the data changes, as by default we maintain the scroll position by key, not index. 399 // when this happens we don't need to reset the animations as from the user perspective 400 // we didn't scroll anywhere and if there is an offset change for an item, this change 401 // should be animated. 402 // however, when the request is to really scroll to a different position, we have to 403 // reset previously known item positions as we don't want offset changes to be animated. 404 // this offset should be considered as a scroll, not the placement change. 405 if (positionChanged) { 406 itemAnimator.reset() 407 } 408 val layoutInfo = layoutInfoState.value 409 val visibleItem = layoutInfo.findVisibleItem(index) 410 if (visibleItem != null && positionChanged) { 411 val currentOffset = 412 if (layoutInfo.orientation == Orientation.Vertical) { 413 visibleItem.offset.y 414 } else { 415 visibleItem.offset.x 416 } 417 val delta = currentOffset + scrollOffset 418 val offsets = 419 IntArray(layoutInfo.firstVisibleItemScrollOffsets.size) { 420 layoutInfo.firstVisibleItemScrollOffsets[it] + delta 421 } 422 scrollPosition.updateScrollOffset(offsets) 423 } else { 424 scrollPosition.requestPositionAndForgetLastKnownKey(index, scrollOffset) 425 } 426 if (forceRemeasure) { 427 remeasurement?.forceRemeasure() 428 } else { 429 measurementScopeInvalidator.invalidateScope() 430 } 431 } 432 433 /** Maintain scroll position for item based on custom key if its index has changed. */ updateScrollPositionIfTheFirstItemWasMovednull434 internal fun updateScrollPositionIfTheFirstItemWasMoved( 435 itemProvider: LazyLayoutItemProvider, 436 firstItemIndex: IntArray 437 ): IntArray = 438 scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) 439 440 override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta) 441 442 /** Start prefetch of the items based on provided delta */ 443 private fun notifyPrefetch( 444 delta: Float, 445 info: LazyStaggeredGridMeasureResult = layoutInfoState.value 446 ) { 447 if (prefetchingEnabled && info.visibleItemsInfo.isNotEmpty()) { 448 val scrollingForward = delta < 0 449 450 val prefetchIndex = 451 if (scrollingForward) { 452 info.visibleItemsInfo.last().index 453 } else { 454 info.visibleItemsInfo.first().index 455 } 456 457 if (prefetchIndex == prefetchBaseIndex) { 458 // Already prefetched based on this index 459 return 460 } 461 prefetchBaseIndex = prefetchIndex 462 463 val prefetchHandlesUsed = mutableSetOf<Int>() 464 var targetIndex = prefetchIndex 465 val slots = info.slots 466 val laneCount = slots.sizes.size 467 for (lane in 0 until laneCount) { 468 val previousIndex = targetIndex 469 470 // find the next item for each line and prefetch if it is valid 471 targetIndex = 472 if (scrollingForward) { 473 laneInfo.findNextItemIndex(previousIndex, lane) 474 } else { 475 laneInfo.findPreviousItemIndex(previousIndex, lane) 476 } 477 if ( 478 targetIndex !in (0 until info.totalItemsCount) || 479 targetIndex in prefetchHandlesUsed 480 ) { 481 break 482 } 483 484 prefetchHandlesUsed += targetIndex 485 if (targetIndex in currentItemPrefetchHandles) { 486 continue 487 } 488 489 val isFullSpan = info.spanProvider.isFullSpan(targetIndex) 490 val slot = if (isFullSpan) 0 else lane 491 val span = if (isFullSpan) laneCount else 1 492 493 val crossAxisSize = 494 when { 495 span == 1 -> slots.sizes[slot] 496 else -> { 497 val start = slots.positions[slot] 498 val endSlot = slot + span - 1 499 val end = slots.positions[endSlot] + slots.sizes[endSlot] 500 end - start 501 } 502 } 503 504 val constraints = 505 if (info.orientation == Orientation.Vertical) { 506 Constraints.fixedWidth(crossAxisSize) 507 } else { 508 Constraints.fixedHeight(crossAxisSize) 509 } 510 511 currentItemPrefetchHandles[targetIndex] = 512 prefetchState.schedulePrecompositionAndPremeasure( 513 index = targetIndex, 514 constraints = constraints 515 ) 516 } 517 518 clearLeftoverPrefetchHandles(prefetchHandlesUsed) 519 } 520 } 521 clearLeftoverPrefetchHandlesnull522 private fun clearLeftoverPrefetchHandles(prefetchHandlesUsed: Set<Int>) { 523 val iterator = currentItemPrefetchHandles.iterator() 524 while (iterator.hasNext()) { 525 val entry = iterator.next() 526 if (entry.key !in prefetchHandlesUsed) { 527 entry.value.cancel() 528 iterator.remove() 529 } 530 } 531 } 532 cancelPrefetchIfVisibleItemsChangednull533 private fun cancelPrefetchIfVisibleItemsChanged(info: LazyStaggeredGridLayoutInfo) { 534 val items = info.visibleItemsInfo 535 if (prefetchBaseIndex != -1 && items.isNotEmpty()) { 536 if (prefetchBaseIndex !in items.first().index..items.last().index) { 537 prefetchBaseIndex = -1 538 currentItemPrefetchHandles.values.forEach { it.cancel() } 539 currentItemPrefetchHandles.clear() 540 } 541 } 542 } 543 544 /** updates state after measure pass */ applyMeasureResultnull545 internal fun applyMeasureResult( 546 result: LazyStaggeredGridMeasureResult, 547 isLookingAhead: Boolean, 548 visibleItemsStayedTheSame: Boolean = false 549 ) { 550 if (!isLookingAhead && hasLookaheadOccurred) { 551 // If there was already a lookahead pass, record this result as Approach result 552 approachLayoutInfo = result 553 } else { 554 if (isLookingAhead) { 555 hasLookaheadOccurred = true 556 } 557 scrollToBeConsumed -= result.consumedScroll 558 layoutInfoState.value = result 559 560 if (visibleItemsStayedTheSame) { 561 scrollPosition.updateScrollOffset(result.firstVisibleItemScrollOffsets) 562 } else { 563 scrollPosition.updateFromMeasureResult(result) 564 cancelPrefetchIfVisibleItemsChanged(result) 565 } 566 canScrollBackward = result.canScrollBackward 567 canScrollForward = result.canScrollForward 568 569 if (isLookingAhead) { 570 _lazyLayoutScrollDeltaBetweenPasses.updateScrollDeltaForApproach( 571 result.scrollBackAmount, 572 result.density, 573 result.coroutineScope 574 ) 575 } 576 measurePassCount++ 577 } 578 } 579 580 internal val scrollDeltaBetweenPasses: Float 581 get() = _lazyLayoutScrollDeltaBetweenPasses.scrollDeltaBetweenPasses 582 583 private val _lazyLayoutScrollDeltaBetweenPasses = LazyLayoutScrollDeltaBetweenPasses() 584 fillNearestIndicesnull585 private fun fillNearestIndices(itemIndex: Int, laneCount: Int): IntArray { 586 val indices = IntArray(laneCount) 587 if (layoutInfoState.value.spanProvider.isFullSpan(itemIndex)) { 588 indices.fill(itemIndex) 589 return indices 590 } 591 592 // reposition spans if needed to ensure valid indices 593 laneInfo.ensureValidIndex(itemIndex + laneCount) 594 val targetLaneIndex = 595 when (val previousLane = laneInfo.getLane(itemIndex)) { 596 // lane was never set or contains obsolete full span (the check for full span above) 597 Unset, 598 FullSpan -> 0 599 // lane was previously set, keep item to the same lane 600 else -> { 601 requirePrecondition(previousLane >= 0) { 602 "Expected positive lane number, got $previousLane instead." 603 } 604 minOf(previousLane, laneCount) 605 } 606 } 607 608 // fill lanes before starting index 609 var currentItemIndex = itemIndex 610 for (lane in (targetLaneIndex - 1) downTo 0) { 611 indices[lane] = laneInfo.findPreviousItemIndex(currentItemIndex, lane) 612 if (indices[lane] == Unset) { 613 indices.fill(-1, toIndex = lane) 614 break 615 } 616 currentItemIndex = indices[lane] 617 } 618 619 indices[targetLaneIndex] = itemIndex 620 621 // fill lanes after starting index 622 currentItemIndex = itemIndex 623 for (lane in (targetLaneIndex + 1) until laneCount) { 624 indices[lane] = laneInfo.findNextItemIndex(currentItemIndex, lane) 625 currentItemIndex = indices[lane] 626 } 627 628 return indices 629 } 630 631 companion object { 632 /** The default implementation of [Saver] for [LazyStaggeredGridState] */ 633 val Saver = 634 listSaver<LazyStaggeredGridState, IntArray>( statenull635 save = { state -> 636 listOf(state.scrollPosition.indices, state.scrollPosition.scrollOffsets) 637 }, <lambda>null638 restore = { LazyStaggeredGridState(it[0], it[1], null) } 639 ) 640 } 641 } 642