diff options
author | Andrey Kulikov <andreykulikov@google.com> | 2023-05-26 18:21:19 +0100 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-06-06 03:23:16 +0000 |
commit | 09007340b37454fbadc12e7122ff6d7ac1d892ff (patch) | |
tree | f9dff60cedbb2b77aeb751d888f0d48f99dfc2b0 | |
parent | b76b96455de6f7a21c22983d33ca78df07aa7a37 (diff) | |
download | support-09007340b37454fbadc12e7122ff6d7ac1d892ff.tar.gz |
Decrease amount of allocations in lazy layouts
Tested on LazyListScrollingBenchmark.scrollProgrammatically_noNewItems
Before: 1,433,275 ns 255 allocs
After: 1,370,767 ns 191 allocs
Test: existing tests and benchmarks
(cherry picked from https://android-review.googlesource.com/q/commit:6aee2ac85a7f34eb7d583e31e2add6cb365c215b)
Merged-In: I57ce5e07812a5d7b9c336010794b4fda450efc24
Change-Id: I57ce5e07812a5d7b9c336010794b4fda450efc24
27 files changed, 456 insertions, 573 deletions
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt index 05949c78d1d..c7be8b629d2 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt @@ -38,6 +38,7 @@ import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.Placeable import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntOffset @@ -216,8 +217,6 @@ private fun rememberLazyListMeasurePolicy( val contentConstraints = containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding) - state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider) - // Update the state's cached Density state.density = this @@ -254,37 +253,46 @@ private fun rememberLazyListMeasurePolicy( ) } - val measuredItemProvider = LazyListMeasuredItemProvider( + val measuredItemProvider = object : LazyListMeasuredItemProvider( contentConstraints, isVertical, itemProvider, this - ) { index, key, contentType, placeables -> - // we add spaceBetweenItems as an extra spacing for all items apart from the last one so - // the lazy list measuring logic will take it into account. - val spacing = if (index == itemsCount - 1) 0 else spaceBetweenItems - LazyListMeasuredItem( - index = index, - placeables = placeables, - isVertical = isVertical, - horizontalAlignment = horizontalAlignment, - verticalAlignment = verticalAlignment, - layoutDirection = layoutDirection, - reverseLayout = reverseLayout, - beforeContentPadding = beforeContentPadding, - afterContentPadding = afterContentPadding, - spacing = spacing, - visualOffset = visualItemOffset, - key = key, - contentType = contentType - ) + ) { + override fun createItem( + index: Int, + key: Any, + contentType: Any?, + placeables: List<Placeable> + ): LazyListMeasuredItem { + // we add spaceBetweenItems as an extra spacing for all items apart from the last one so + // the lazy list measuring logic will take it into account. + val spacing = if (index == itemsCount - 1) 0 else spaceBetweenItems + return LazyListMeasuredItem( + index = index, + placeables = placeables, + isVertical = isVertical, + horizontalAlignment = horizontalAlignment, + verticalAlignment = verticalAlignment, + layoutDirection = layoutDirection, + reverseLayout = reverseLayout, + beforeContentPadding = beforeContentPadding, + afterContentPadding = afterContentPadding, + spacing = spacing, + visualOffset = visualItemOffset, + key = key, + contentType = contentType + ) + } } state.premeasureConstraints = measuredItemProvider.childConstraints val firstVisibleItemIndex: Int val firstVisibleScrollOffset: Int Snapshot.withoutReadObservation { - firstVisibleItemIndex = state.firstVisibleItemIndex + firstVisibleItemIndex = state.updateScrollPositionIfTheFirstItemWasMoved( + itemProvider, state.firstVisibleItemIndex + ) firstVisibleScrollOffset = state.firstVisibleItemScrollOffset } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt index 978d3a81489..8cf55e5f531 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt @@ -28,13 +28,13 @@ import androidx.compose.ui.util.fastForEachIndexed * @param beforeContentPadding the padding before the first item in the list */ internal fun findOrComposeLazyListHeader( - composedVisibleItems: MutableList<LazyListPositionedItem>, + composedVisibleItems: MutableList<LazyListMeasuredItem>, itemProvider: LazyListMeasuredItemProvider, headerIndexes: List<Int>, beforeContentPadding: Int, layoutWidth: Int, layoutHeight: Int, -): LazyListPositionedItem? { +): LazyListMeasuredItem? { var currentHeaderOffset: Int = Int.MIN_VALUE var nextHeaderOffset: Int = Int.MIN_VALUE @@ -83,11 +83,11 @@ internal fun findOrComposeLazyListHeader( headerOffset = minOf(headerOffset, nextHeaderOffset - measuredHeaderItem.size) } - return measuredHeaderItem.position(headerOffset, layoutWidth, layoutHeight).also { - if (indexInComposedVisibleItems != -1) { - composedVisibleItems[indexInComposedVisibleItems] = it - } else { - composedVisibleItems.add(0, it) - } + measuredHeaderItem.position(headerOffset, layoutWidth, layoutHeight) + if (indexInComposedVisibleItems != -1) { + composedVisibleItems[indexInComposedVisibleItems] = measuredHeaderItem + } else { + composedVisibleItems.add(0, measuredHeaderItem) } + return measuredHeaderItem } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt index 497ba6d4cdc..ec0fb82e7e4 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt @@ -40,8 +40,8 @@ internal class LazyListItemPlacementAnimator { // stored to not allocate it every pass. private val movingAwayKeys = LinkedHashSet<Any>() - private val movingInFromStartBound = mutableListOf<LazyListPositionedItem>() - private val movingInFromEndBound = mutableListOf<LazyListPositionedItem>() + private val movingInFromStartBound = mutableListOf<LazyListMeasuredItem>() + private val movingInFromEndBound = mutableListOf<LazyListMeasuredItem>() private val movingAwayToStartBound = mutableListOf<LazyListMeasuredItem>() private val movingAwayToEndBound = mutableListOf<LazyListMeasuredItem>() @@ -54,7 +54,7 @@ internal class LazyListItemPlacementAnimator { consumedScroll: Int, layoutWidth: Int, layoutHeight: Int, - positionedItems: MutableList<LazyListPositionedItem>, + positionedItems: MutableList<LazyListMeasuredItem>, itemProvider: LazyListMeasuredItemProvider, isVertical: Boolean ) { @@ -167,9 +167,9 @@ internal class LazyListItemPlacementAnimator { accumulatedOffset += item.size val mainAxisOffset = 0 - accumulatedOffset - val positionedItem = item.position(mainAxisOffset, layoutWidth, layoutHeight) - positionedItems.add(positionedItem) - startAnimationsIfNeeded(positionedItem) + item.position(mainAxisOffset, layoutWidth, layoutHeight) + positionedItems.add(item) + startAnimationsIfNeeded(item) } accumulatedOffset = 0 movingAwayToEndBound.sortBy { keyIndexMap.getIndex(it.key) } @@ -177,9 +177,9 @@ internal class LazyListItemPlacementAnimator { val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset accumulatedOffset += item.size - val positionedItem = item.position(mainAxisOffset, layoutWidth, layoutHeight) - positionedItems.add(positionedItem) - startAnimationsIfNeeded(positionedItem) + item.position(mainAxisOffset, layoutWidth, layoutHeight) + positionedItems.add(item) + startAnimationsIfNeeded(item) } movingInFromStartBound.clear() @@ -200,7 +200,7 @@ internal class LazyListItemPlacementAnimator { } private fun initializeNode( - item: LazyListPositionedItem, + item: LazyListMeasuredItem, mainAxisOffset: Int ) { val firstPlaceableOffset = item.getOffset(0) @@ -219,7 +219,7 @@ internal class LazyListItemPlacementAnimator { } } - private fun startAnimationsIfNeeded(item: LazyListPositionedItem) { + private fun startAnimationsIfNeeded(item: LazyListMeasuredItem) { item.forEachNode { placeableIndex, node -> val newTarget = item.getOffset(placeableIndex) val currentTarget = node.rawOffset @@ -234,13 +234,13 @@ internal class LazyListItemPlacementAnimator { private val Any?.node get() = this as? LazyLayoutAnimateItemModifierNode - private val LazyListPositionedItem.hasAnimations: Boolean + private val LazyListMeasuredItem.hasAnimations: Boolean get() { forEachNode { _, _ -> return true } return false } - private inline fun LazyListPositionedItem.forEachNode( + private inline fun LazyListMeasuredItem.forEachNode( block: (placeableIndex: Int, node: LazyLayoutAnimateItemModifierNode) -> Unit ) { repeat(placeablesCount) { index -> diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt index c247a1dcf82..7b4d920828d 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt @@ -102,7 +102,7 @@ internal fun measureLazyList( } // this will contain all the MeasuredItems representing the visible items - val visibleItems = mutableListOf<LazyListMeasuredItem>() + val visibleItems = ArrayDeque<LazyListMeasuredItem>() // define min and max offsets val minOffset = -beforeContentPadding + if (spaceBetweenItems < 0) spaceBetweenItems else 0 @@ -403,7 +403,7 @@ private fun calculateItemsOffsets( horizontalArrangement: Arrangement.Horizontal?, reverseLayout: Boolean, density: Density, -): MutableList<LazyListPositionedItem> { +): MutableList<LazyListMeasuredItem> { val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth val hasSpareSpace = finalMainAxisOffset < minOf(mainAxisLayoutSize, maxOffset) if (hasSpareSpace) { @@ -411,7 +411,7 @@ private fun calculateItemsOffsets( } val positionedItems = - ArrayList<LazyListPositionedItem>(items.size + extraItemsBefore.size + extraItemsAfter.size) + ArrayList<LazyListMeasuredItem>(items.size + extraItemsBefore.size + extraItemsAfter.size) if (hasSpareSpace) { require(extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty()) @@ -447,29 +447,29 @@ private fun calculateItemsOffsets( } else { absoluteOffset } - positionedItems.add(item.position(relativeOffset, layoutWidth, layoutHeight)) + item.position(relativeOffset, layoutWidth, layoutHeight) + positionedItems.add(item) } } else { var currentMainAxis = itemsScrollOffset extraItemsBefore.fastForEach { currentMainAxis -= it.sizeWithSpacings - positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight)) + it.position(currentMainAxis, layoutWidth, layoutHeight) + positionedItems.add(it) } currentMainAxis = itemsScrollOffset items.fastForEach { - positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight)) + it.position(currentMainAxis, layoutWidth, layoutHeight) + positionedItems.add(it) currentMainAxis += it.sizeWithSpacings } extraItemsAfter.fastForEach { - positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight)) + it.position(currentMainAxis, layoutWidth, layoutHeight) + positionedItems.add(it) currentMainAxis += it.sizeWithSpacings } } return positionedItems } - -private val EmptyRange = Int.MIN_VALUE to Int.MIN_VALUE -private val Int.notInEmptyRange - get() = this != Int.MIN_VALUE
\ No newline at end of file diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt index 0c4b130f962..5fb7dc952f8 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt @@ -23,15 +23,16 @@ import androidx.compose.ui.layout.Placeable import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed /** * Represents one measured item of the lazy list. It can in fact consist of multiple placeables * if the user emit multiple layout nodes in the item callback. */ internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor( - val index: Int, + override val index: Int, private val placeables: List<Placeable>, - private val isVertical: Boolean, + val isVertical: Boolean, private val horizontalAlignment: Alignment.Horizontal?, private val verticalAlignment: Alignment.Vertical?, private val layoutDirection: LayoutDirection, @@ -48,13 +49,16 @@ internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor( * value passed into the place() call. */ private val visualOffset: IntOffset, - val key: Any, - private val contentType: Any? -) { + override val key: Any, + override val contentType: Any? +) : LazyListItemInfo { + override var offset: Int = 0 + private set + /** * Sum of the main axis sizes of all the inner placeables. */ - val size: Int + override val size: Int /** * Sum of the main axis sizes of all the inner placeables and [spacing]. @@ -66,6 +70,14 @@ internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor( */ val crossAxisSize: Int + private var mainAxisLayoutSize: Int = Unset + private var minMainAxisOffset: Int = 0 + private var maxMainAxisOffset: Int = 0 + + // optimized for storing x and y offsets for each placeable one by one. + // array's size == placeables.size * 2, first we store x, then y. + private val placeableOffsets: IntArray + init { var mainAxisSize = 0 var maxCrossAxis = 0 @@ -76,6 +88,7 @@ internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor( size = mainAxisSize sizeWithSpacings = (size + spacing).coerceAtLeast(0) crossAxisSize = maxCrossAxis + placeableOffsets = IntArray(placeables.size * 2) } val placeablesCount: Int get() = placeables.size @@ -90,64 +103,37 @@ internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor( offset: Int, layoutWidth: Int, layoutHeight: Int - ): LazyListPositionedItem { - val wrappers = mutableListOf<LazyListPlaceableWrapper>() - val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth + ) { + this.offset = offset + mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth var mainAxisOffset = offset - placeables.fastForEach { - val placeableOffset = if (isVertical) { - val x = requireNotNull(horizontalAlignment) - .align(it.width, layoutWidth, layoutDirection) - IntOffset(x, mainAxisOffset) + placeables.fastForEachIndexed { index, placeable -> + val indexInArray = index * 2 + if (isVertical) { + placeableOffsets[indexInArray] = requireNotNull(horizontalAlignment) + .align(placeable.width, layoutWidth, layoutDirection) + placeableOffsets[indexInArray + 1] = mainAxisOffset + mainAxisOffset += placeable.height } else { - val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight) - IntOffset(mainAxisOffset, y) + placeableOffsets[indexInArray] = mainAxisOffset + placeableOffsets[indexInArray + 1] = requireNotNull(verticalAlignment) + .align(placeable.height, layoutHeight) + mainAxisOffset += placeable.width } - mainAxisOffset += if (isVertical) it.height else it.width - wrappers.add(LazyListPlaceableWrapper(placeableOffset, it)) } - return LazyListPositionedItem( - offset = offset, - index = this.index, - key = key, - size = size, - minMainAxisOffset = -beforeContentPadding, - maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding, - isVertical = isVertical, - wrappers = wrappers, - visualOffset = visualOffset, - reverseLayout = reverseLayout, - mainAxisLayoutSize = mainAxisLayoutSize, - contentType = contentType - ) + minMainAxisOffset = -beforeContentPadding + maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding } -} - -internal class LazyListPositionedItem( - override val offset: Int, - override val index: Int, - override val key: Any, - override val size: Int, - private val minMainAxisOffset: Int, - private val maxMainAxisOffset: Int, - val isVertical: Boolean, - private val wrappers: List<LazyListPlaceableWrapper>, - private val visualOffset: IntOffset, - private val reverseLayout: Boolean, - private val mainAxisLayoutSize: Int, - override val contentType: Any? -) : LazyListItemInfo { - val placeablesCount: Int get() = wrappers.size - - fun getOffset(index: Int) = wrappers[index].offset - fun getParentData(index: Int) = wrappers[index].placeable.parentData + fun getOffset(index: Int) = + IntOffset(placeableOffsets[index * 2], placeableOffsets[index * 2 + 1]) fun place( scope: Placeable.PlacementScope, ) = with(scope) { + require(mainAxisLayoutSize != Unset) { "position() should be called first" } repeat(placeablesCount) { index -> - val placeable = wrappers[index].placeable + val placeable = placeables[index] val minOffset = minMainAxisOffset - placeable.mainAxisSize val maxOffset = maxMainAxisOffset var offset = getOffset(index) @@ -182,7 +168,4 @@ internal class LazyListPositionedItem( IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y) } -internal class LazyListPlaceableWrapper( - val offset: IntOffset, - val placeable: Placeable -) +private const val Unset = Int.MIN_VALUE
\ No newline at end of file diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt index 77ec0760a3f..7c7a0ce5a32 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt @@ -26,12 +26,11 @@ import androidx.compose.ui.unit.Constraints * Abstracts away the subcomposition from the measuring logic. */ @OptIn(ExperimentalFoundationApi::class) -internal class LazyListMeasuredItemProvider @ExperimentalFoundationApi constructor( +internal abstract class LazyListMeasuredItemProvider @ExperimentalFoundationApi constructor( constraints: Constraints, isVertical: Boolean, private val itemProvider: LazyListItemProvider, - private val measureScope: LazyLayoutMeasureScope, - private val measuredItemFactory: MeasuredItemFactory + private val measureScope: LazyLayoutMeasureScope ) { // the constraints we will measure child with. the main axis is not restricted val childConstraints = Constraints( @@ -47,7 +46,7 @@ internal class LazyListMeasuredItemProvider @ExperimentalFoundationApi construct val key = keyIndexMap.getKey(index) ?: itemProvider.getKey(index) val contentType = itemProvider.getContentType(index) val placeables = measureScope.measure(index, childConstraints) - return measuredItemFactory.createItem(index, key, contentType, placeables) + return createItem(index, key, contentType, placeables) } /** @@ -55,11 +54,8 @@ internal class LazyListMeasuredItemProvider @ExperimentalFoundationApi construct * the list as an optimization. **/ val keyIndexMap: LazyLayoutKeyIndexMap = itemProvider.keyIndexMap -} -// This interface allows to avoid autoboxing on index param -internal fun interface MeasuredItemFactory { - fun createItem( + abstract fun createItem( index: Int, key: Any, contentType: Any?, diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt index 16eb1c71ecb..744089fa22b 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.lazy.layout.findIndexByKey import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.Snapshot /** * Contains the current scroll position represented by the first visible item index and the first @@ -54,12 +53,8 @@ internal class LazyListScrollPosition( val scrollOffset = measureResult.firstVisibleItemScrollOffset check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" } - Snapshot.withoutReadObservation { - update( - measureResult.firstVisibleItem?.index ?: 0, - scrollOffset - ) - } + val firstIndex = measureResult.firstVisibleItem?.index ?: 0 + update(firstIndex, scrollOffset) } } @@ -88,22 +83,18 @@ internal class LazyListScrollPosition( * as the first visible one even given that its index has been changed. */ @ExperimentalFoundationApi - fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) { - Snapshot.withoutReadObservation { - update( - itemProvider.findIndexByKey(lastKnownFirstItemKey, index), - scrollOffset - ) - } + fun updateScrollPositionIfTheFirstItemWasMoved( + itemProvider: LazyListItemProvider, + index: Int + ): Int { + val newIndex = itemProvider.findIndexByKey(lastKnownFirstItemKey, index) + this.index = newIndex + return newIndex } private fun update(index: Int, scrollOffset: Int) { require(index >= 0f) { "Index should be non-negative ($index)" } - if (index != this.index) { - this.index = index - } - if (scrollOffset != this.scrollOffset) { - this.scrollOffset = scrollOffset - } + this.index = index + this.scrollOffset = scrollOffset } } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt index 3bd36be0054..41384da64a3 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.layout.Remeasurement import androidx.compose.ui.layout.RemeasurementModifier import androidx.compose.ui.unit.Constraints @@ -152,7 +153,7 @@ class LazyListState constructor( /** * Needed for [animateScrollToItem]. Updated on every measure. */ - internal var density: Density by mutableStateOf(Density(1f, 1f)) + internal var density: Density = Density(1f, 1f) /** * The ScrollableController instance. We keep it as we need to call stopAnimation on it once @@ -193,7 +194,7 @@ class LazyListState constructor( * The [Remeasurement] object associated with our layout. It allows us to remeasure * synchronously during scroll. */ - internal var remeasurement: Remeasurement? by mutableStateOf(null) + internal var remeasurement: Remeasurement? = null private set /** @@ -218,7 +219,7 @@ class LazyListState constructor( /** * Constraints passed to the prefetcher for premeasuring the prefetched items. */ - internal var premeasureConstraints by mutableStateOf(Constraints()) + internal var premeasureConstraints = Constraints() /** * Stores currently pinned items which are always composed. @@ -401,9 +402,10 @@ class LazyListState constructor( * items added or removed before our current first visible item and keep this item * as the first visible one even given that its index has been changed. */ - internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) { - scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider) - } + internal fun updateScrollPositionIfTheFirstItemWasMoved( + itemProvider: LazyListItemProvider, + firstItemIndex: Int = Snapshot.withoutReadObservation { scrollPosition.index } + ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) companion object { /** diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt index 4f84478fc76..ac446afbbac 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.Placeable import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density @@ -215,8 +216,6 @@ private fun rememberLazyGridMeasurePolicy( val contentConstraints = containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding) - state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider) - val spanLayoutProvider = itemProvider.spanLayoutProvider val resolvedSlots = slots(containerConstraints) val slotsPerLine = resolvedSlots.sizes.size @@ -252,12 +251,19 @@ private fun rememberLazyGridMeasurePolicy( ) } - val measuredItemProvider = LazyGridMeasuredItemProvider( + val measuredItemProvider = object : LazyGridMeasuredItemProvider( itemProvider, this, spaceBetweenLines - ) { index, key, contentType, crossAxisSize, mainAxisSpacing, placeables -> - LazyGridMeasuredItem( + ) { + override fun createItem( + index: Int, + key: Any, + contentType: Any?, + crossAxisSize: Int, + mainAxisSpacing: Int, + placeables: List<Placeable> + ) = LazyGridMeasuredItem( index = index, key = key, isVertical = isVertical, @@ -272,15 +278,20 @@ private fun rememberLazyGridMeasurePolicy( contentType = contentType ) } - val measuredLineProvider = LazyGridMeasuredLineProvider( + val measuredLineProvider = object : LazyGridMeasuredLineProvider( isVertical = isVertical, slots = resolvedSlots, gridItemsCount = itemsCount, spaceBetweenLines = spaceBetweenLines, measuredItemProvider = measuredItemProvider, spanLayoutProvider = spanLayoutProvider - ) { index, items, spans, mainAxisSpacing -> - LazyGridMeasuredLine( + ) { + override fun createLine( + index: Int, + items: Array<LazyGridMeasuredItem>, + spans: List<GridItemSpan>, + mainAxisSpacing: Int + ) = LazyGridMeasuredLine( index = index, items = items, spans = spans, @@ -307,10 +318,11 @@ private fun rememberLazyGridMeasurePolicy( val firstVisibleLineScrollOffset: Int Snapshot.withoutReadObservation { - if (state.firstVisibleItemIndex < itemsCount || itemsCount <= 0) { - firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem( - state.firstVisibleItemIndex - ) + val index = state.updateScrollPositionIfTheFirstItemWasMoved( + itemProvider, state.firstVisibleItemIndex + ) + if (index < itemsCount || itemsCount <= 0) { + firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(index) firstVisibleLineScrollOffset = state.firstVisibleItemScrollOffset } else { // the data set has been updated and now we have less items that we were diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt index fbd96ee7e8a..918f2a2881f 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt @@ -41,8 +41,8 @@ internal class LazyGridItemPlacementAnimator { // stored to not allocate it every pass. private val movingAwayKeys = LinkedHashSet<Any>() - private val movingInFromStartBound = mutableListOf<LazyGridPositionedItem>() - private val movingInFromEndBound = mutableListOf<LazyGridPositionedItem>() + private val movingInFromStartBound = mutableListOf<LazyGridMeasuredItem>() + private val movingInFromEndBound = mutableListOf<LazyGridMeasuredItem>() private val movingAwayToStartBound = mutableListOf<LazyGridMeasuredItem>() private val movingAwayToEndBound = mutableListOf<LazyGridMeasuredItem>() @@ -55,7 +55,7 @@ internal class LazyGridItemPlacementAnimator { consumedScroll: Int, layoutWidth: Int, layoutHeight: Int, - positionedItems: MutableList<LazyGridPositionedItem>, + positionedItems: MutableList<LazyGridMeasuredItem>, itemProvider: LazyGridMeasuredItemProvider, spanLayoutProvider: LazyGridSpanLayoutProvider, isVertical: Boolean @@ -91,7 +91,7 @@ internal class LazyGridItemPlacementAnimator { // there is no state associated with this item yet if (itemInfo == null) { keyToItemInfoMap[item.key] = - ItemInfo(item.getCrossAxisSize(), item.getCrossAxisOffset()) + ItemInfo(item.crossAxisSize, item.crossAxisOffset) val previousIndex = previousKeyToIndexMap.getIndex(item.key) if (previousIndex != -1 && item.index != previousIndex) { if (previousIndex < previousFirstVisibleIndex) { @@ -112,8 +112,8 @@ internal class LazyGridItemPlacementAnimator { it.rawOffset += scrollOffset } } - itemInfo.crossAxisSize = item.getCrossAxisSize() - itemInfo.crossAxisOffset = item.getCrossAxisOffset() + itemInfo.crossAxisSize = item.crossAxisSize + itemInfo.crossAxisOffset = item.crossAxisOffset startAnimationsIfNeeded(item) } } else { @@ -129,13 +129,13 @@ internal class LazyGridItemPlacementAnimator { movingInFromStartBound.fastForEach { item -> val line = if (isVertical) item.row else item.column if (line != -1 && line == previousLine) { - previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.getMainAxisSize()) + previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize) } else { accumulatedOffset += previousLineMainAxisSize - previousLineMainAxisSize = item.getMainAxisSize() + previousLineMainAxisSize = item.mainAxisSize previousLine = line } - val mainAxisOffset = 0 - accumulatedOffset - item.getMainAxisSize() + val mainAxisOffset = 0 - accumulatedOffset - item.mainAxisSize initializeNode(item, mainAxisOffset) startAnimationsIfNeeded(item) } @@ -146,10 +146,10 @@ internal class LazyGridItemPlacementAnimator { movingInFromEndBound.fastForEach { item -> val line = if (isVertical) item.row else item.column if (line != -1 && line == previousLine) { - previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.getMainAxisSize()) + previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize) } else { accumulatedOffset += previousLineMainAxisSize - previousLineMainAxisSize = item.getMainAxisSize() + previousLineMainAxisSize = item.mainAxisSize previousLine = line } val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset @@ -211,16 +211,14 @@ internal class LazyGridItemPlacementAnimator { val itemInfo = keyToItemInfoMap.getValue(item.key) - val positionedItem = item.position( - mainAxisOffset, - itemInfo.crossAxisOffset, - layoutWidth, - layoutHeight, - LazyGridItemInfo.UnknownRow, - LazyGridItemInfo.UnknownColumn + item.position( + mainAxisOffset = mainAxisOffset, + crossAxisOffset = itemInfo.crossAxisOffset, + layoutWidth = layoutWidth, + layoutHeight = layoutHeight ) - positionedItems.add(positionedItem) - startAnimationsIfNeeded(positionedItem) + positionedItems.add(item) + startAnimationsIfNeeded(item) } accumulatedOffset = 0 previousLine = -1 @@ -238,17 +236,15 @@ internal class LazyGridItemPlacementAnimator { val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset val itemInfo = keyToItemInfoMap.getValue(item.key) - val positionedItem = item.position( - mainAxisOffset, - itemInfo.crossAxisOffset, - layoutWidth, - layoutHeight, - LazyGridItemInfo.UnknownRow, - LazyGridItemInfo.UnknownColumn + item.position( + mainAxisOffset = mainAxisOffset, + crossAxisOffset = itemInfo.crossAxisOffset, + layoutWidth = layoutWidth, + layoutHeight = layoutHeight, ) - positionedItems.add(positionedItem) - startAnimationsIfNeeded(positionedItem) + positionedItems.add(item) + startAnimationsIfNeeded(item) } movingInFromStartBound.clear() @@ -269,7 +265,7 @@ internal class LazyGridItemPlacementAnimator { } private fun initializeNode( - item: LazyGridPositionedItem, + item: LazyGridMeasuredItem, mainAxisOffset: Int ) { val firstPlaceableOffset = item.offset @@ -288,7 +284,7 @@ internal class LazyGridItemPlacementAnimator { } } - private fun startAnimationsIfNeeded(item: LazyGridPositionedItem) { + private fun startAnimationsIfNeeded(item: LazyGridMeasuredItem) { item.forEachNode { node -> val newTarget = item.offset val currentTarget = node.rawOffset @@ -303,13 +299,13 @@ internal class LazyGridItemPlacementAnimator { private val Any?.node get() = this as? LazyLayoutAnimateItemModifierNode - private val LazyGridPositionedItem.hasAnimations: Boolean + private val LazyGridMeasuredItem.hasAnimations: Boolean get() { forEachNode { return true } return false } - private inline fun LazyGridPositionedItem.forEachNode( + private inline fun LazyGridMeasuredItem.forEachNode( block: (LazyLayoutAnimateItemModifierNode) -> Unit ) { repeat(placeablesCount) { index -> diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt index d5fa3ecadbf..b9258895247 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt @@ -98,7 +98,7 @@ internal fun measureLazyGrid( } // this will contain all the MeasuredItems representing the visible lines - val visibleLines = mutableListOf<LazyGridMeasuredLine>() + val visibleLines = ArrayDeque<LazyGridMeasuredLine>() // define min and max offsets val minOffset = -beforeContentPadding + if (spaceBetweenLines < 0) spaceBetweenLines else 0 @@ -343,14 +343,14 @@ private fun calculateItemsOffsets( horizontalArrangement: Arrangement.Horizontal?, reverseLayout: Boolean, density: Density, -): MutableList<LazyGridPositionedItem> { +): MutableList<LazyGridMeasuredItem> { val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth val hasSpareSpace = finalMainAxisOffset < min(mainAxisLayoutSize, maxOffset) if (hasSpareSpace) { check(firstLineScrollOffset == 0) } - val positionedItems = ArrayList<LazyGridPositionedItem>(lines.fastSumBy { it.items.size }) + val positionedItems = ArrayList<LazyGridMeasuredItem>(lines.fastSumBy { it.items.size }) if (hasSpareSpace) { require(itemsBefore.isEmpty() && itemsAfter.isEmpty()) @@ -395,7 +395,8 @@ private fun calculateItemsOffsets( itemsBefore.fastForEach { currentMainAxis -= it.mainAxisSizeWithSpacings - positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight)) + it.position(currentMainAxis, 0, layoutWidth, layoutHeight) + positionedItems.add(it) } currentMainAxis = firstLineScrollOffset @@ -405,23 +406,10 @@ private fun calculateItemsOffsets( } itemsAfter.fastForEach { - positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight)) + it.position(currentMainAxis, 0, layoutWidth, layoutHeight) + positionedItems.add(it) currentMainAxis += it.mainAxisSizeWithSpacings } } return positionedItems } - -private fun LazyGridMeasuredItem.positionExtraItem( - mainAxisOffset: Int, - layoutWidth: Int, - layoutHeight: Int -): LazyGridPositionedItem = - position( - mainAxisOffset = mainAxisOffset, - crossAxisOffset = 0, - layoutWidth = layoutWidth, - layoutHeight = layoutHeight, - row = -1, - column = -1 - ) diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt index 74a6896e2c9..ecf9ff7f2ec 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt @@ -28,27 +28,27 @@ import androidx.compose.ui.util.fastForEach * if the user emit multiple layout nodes in the item callback. */ internal class LazyGridMeasuredItem( - val index: Int, - val key: Any, - private val isVertical: Boolean, + override val index: Int, + override val key: Any, + val isVertical: Boolean, /** * Cross axis size is the same for all [placeables]. Take it as parameter for the case when * [placeables] is empty. */ val crossAxisSize: Int, - val mainAxisSpacing: Int, + mainAxisSpacing: Int, private val reverseLayout: Boolean, private val layoutDirection: LayoutDirection, private val beforeContentPadding: Int, private val afterContentPadding: Int, - val placeables: List<Placeable>, + private val placeables: List<Placeable>, /** * The offset which shouldn't affect any calculations but needs to be applied for the final * value passed into the place() call. */ private val visualOffset: IntOffset, - private val contentType: Any? -) { + override val contentType: Any? +) : LazyGridItemInfo { /** * Main axis size of the item - the max main axis size of the placeables. */ @@ -61,6 +61,10 @@ internal class LazyGridMeasuredItem( val placeablesCount: Int get() = placeables.size + private var mainAxisLayoutSize: Int = Unset + private var minMainAxisOffset: Int = 0 + private var maxMainAxisOffset: Int = 0 + fun getParentData(index: Int) = placeables[index].parentData init { @@ -72,6 +76,19 @@ internal class LazyGridMeasuredItem( mainAxisSizeWithSpacings = (maxMainAxis + mainAxisSpacing).coerceAtLeast(0) } + override val size: IntSize = if (isVertical) { + IntSize(crossAxisSize, mainAxisSize) + } else { + IntSize(mainAxisSize, crossAxisSize) + } + override var offset: IntOffset = IntOffset.Zero + private set + val crossAxisOffset get() = if (isVertical) offset.x else offset.y + override var row: Int = LazyGridItemInfo.UnknownRow + private set + override var column: Int = LazyGridItemInfo.UnknownColumn + private set + /** * Calculates positions for the inner placeables at [mainAxisOffset], [crossAxisOffset]. * [layoutWidth] and [layoutHeight] should be provided to not place placeables which are ended @@ -84,10 +101,10 @@ internal class LazyGridMeasuredItem( crossAxisOffset: Int, layoutWidth: Int, layoutHeight: Int, - row: Int, - column: Int - ): LazyGridPositionedItem { - val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth + row: Int = LazyGridItemInfo.UnknownRow, + column: Int = LazyGridItemInfo.UnknownColumn + ) { + mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight @Suppress("NAME_SHADOWING") val crossAxisOffset = if (isVertical && layoutDirection == LayoutDirection.Rtl) { @@ -95,62 +112,21 @@ internal class LazyGridMeasuredItem( } else { crossAxisOffset } - return LazyGridPositionedItem( - offset = if (isVertical) { - IntOffset(crossAxisOffset, mainAxisOffset) - } else { - IntOffset(mainAxisOffset, crossAxisOffset) - }, - index = index, - key = key, - row = row, - column = column, - size = if (isVertical) { - IntSize(crossAxisSize, mainAxisSize) - } else { - IntSize(mainAxisSize, crossAxisSize) - }, - minMainAxisOffset = -beforeContentPadding, - maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding, - isVertical = isVertical, - placeables = placeables, - visualOffset = visualOffset, - mainAxisLayoutSize = mainAxisLayoutSize, - reverseLayout = reverseLayout, - contentType = contentType - ) + offset = if (isVertical) { + IntOffset(crossAxisOffset, mainAxisOffset) + } else { + IntOffset(mainAxisOffset, crossAxisOffset) + } + this.row = row + this.column = column + minMainAxisOffset = -beforeContentPadding + maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding } -} - -internal class LazyGridPositionedItem( - override val offset: IntOffset, - override val index: Int, - override val key: Any, - override val row: Int, - override val column: Int, - override val size: IntSize, - private val minMainAxisOffset: Int, - private val maxMainAxisOffset: Int, - val isVertical: Boolean, - private val placeables: List<Placeable>, - private val visualOffset: IntOffset, - private val mainAxisLayoutSize: Int, - private val reverseLayout: Boolean, - override val contentType: Any? -) : LazyGridItemInfo { - val placeablesCount: Int get() = placeables.size - - fun getMainAxisSize() = if (isVertical) size.height else size.width - - fun getCrossAxisSize() = if (isVertical) size.width else size.height - - fun getCrossAxisOffset() = if (isVertical) offset.x else offset.y - - fun getParentData(index: Int) = placeables[index].parentData fun place( scope: Placeable.PlacementScope, ) = with(scope) { + require(mainAxisLayoutSize != Unset) { "position() should be called first" } repeat(placeablesCount) { index -> val placeable = placeables[index] val minOffset = minMainAxisOffset - placeable.mainAxisSize @@ -187,3 +163,5 @@ internal class LazyGridPositionedItem( private inline fun IntOffset.copy(mainAxisMap: (Int) -> Int): IntOffset = IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y) } + +private const val Unset = Int.MIN_VALUE
\ No newline at end of file diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt index c9846316c71..18d87a3e284 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt @@ -26,11 +26,10 @@ import androidx.compose.ui.unit.Constraints * Abstracts away the subcomposition from the measuring logic. */ @OptIn(ExperimentalFoundationApi::class) -internal class LazyGridMeasuredItemProvider @ExperimentalFoundationApi constructor( +internal abstract class LazyGridMeasuredItemProvider @ExperimentalFoundationApi constructor( private val itemProvider: LazyGridItemProvider, private val measureScope: LazyLayoutMeasureScope, - private val defaultMainAxisSpacing: Int, - private val measuredItemFactory: MeasuredItemFactory + private val defaultMainAxisSpacing: Int ) { /** * Used to subcompose individual items of lazy grids. Composed placeables will be measured @@ -50,7 +49,7 @@ internal class LazyGridMeasuredItemProvider @ExperimentalFoundationApi construct require(constraints.hasFixedHeight) constraints.minHeight } - return measuredItemFactory.createItem( + return createItem( index, key, contentType, @@ -65,11 +64,8 @@ internal class LazyGridMeasuredItemProvider @ExperimentalFoundationApi construct * the list as an optimization. **/ val keyIndexMap: LazyLayoutKeyIndexMap = itemProvider.keyIndexMap -} -// This interface allows to avoid autoboxing on index param -internal fun interface MeasuredItemFactory { - fun createItem( + abstract fun createItem( index: Int, key: Any, contentType: Any?, diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt index 0b3f5e04527..f5f8349b8a1 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt @@ -66,9 +66,9 @@ internal class LazyGridMeasuredLine constructor( offset: Int, layoutWidth: Int, layoutHeight: Int - ): List<LazyGridPositionedItem> { + ): Array<LazyGridMeasuredItem> { var usedSpan = 0 - return items.mapIndexed { itemIndex, item -> + items.forEachIndexed { itemIndex, item -> val span = spans[itemIndex].currentLineSpan val startSlot = usedSpan @@ -83,5 +83,6 @@ internal class LazyGridMeasuredLine constructor( usedSpan += span } } + return items } } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt index fe5ff5498b3..c852c3f3fca 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt @@ -24,14 +24,13 @@ import androidx.compose.ui.unit.Constraints * Abstracts away subcomposition and span calculation from the measuring logic of entire lines. */ @OptIn(ExperimentalFoundationApi::class) -internal class LazyGridMeasuredLineProvider( +internal abstract class LazyGridMeasuredLineProvider( private val isVertical: Boolean, private val slots: LazyGridSlots, private val gridItemsCount: Int, private val spaceBetweenLines: Int, private val measuredItemProvider: LazyGridMeasuredItemProvider, - private val spanLayoutProvider: LazyGridSpanLayoutProvider, - private val measuredLineFactory: MeasuredLineFactory + private val spanLayoutProvider: LazyGridSpanLayoutProvider ) { // The constraints for cross axis size. The main axis is not restricted. internal fun childConstraints(startSlot: Int, span: Int): Constraints { @@ -67,7 +66,8 @@ internal class LazyGridMeasuredLineProvider( // we add space between lines as an extra spacing for all lines apart from the last one // so the lazy grid measuring logic will take it into account. val mainAxisSpacing = if (lineItemsCount == 0 || - lineConfiguration.firstItemIndex + lineItemsCount == gridItemsCount) { + lineConfiguration.firstItemIndex + lineItemsCount == gridItemsCount + ) { 0 } else { spaceBetweenLines @@ -83,7 +83,7 @@ internal class LazyGridMeasuredLineProvider( constraints ).also { startSlot += span } } - return measuredLineFactory.createLine( + return createLine( lineIndex, items, lineConfiguration.spans, @@ -96,12 +96,8 @@ internal class LazyGridMeasuredLineProvider( * the list as an optimization. **/ val keyIndexMap: LazyLayoutKeyIndexMap get() = measuredItemProvider.keyIndexMap -} -// This interface allows to avoid autoboxing on index param -@OptIn(ExperimentalFoundationApi::class) -internal fun interface MeasuredLineFactory { - fun createLine( + abstract fun createLine( index: Int, items: Array<LazyGridMeasuredItem>, spans: List<GridItemSpan>, diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt index 7b196194664..c483a243934 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.lazy.layout.findIndexByKey import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.Snapshot /** * Contains the current scroll position represented by the first visible item index and the first @@ -56,12 +55,8 @@ internal class LazyGridScrollPosition( val scrollOffset = measureResult.firstVisibleLineScrollOffset check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" } - Snapshot.withoutReadObservation { - update( - measureResult.firstVisibleLine?.items?.firstOrNull()?.index ?: 0, - scrollOffset - ) - } + val firstIndex = measureResult.firstVisibleLine?.items?.firstOrNull()?.index ?: 0 + update(firstIndex, scrollOffset) } } @@ -89,22 +84,18 @@ internal class LazyGridScrollPosition( * there were items added or removed before our current first visible item and keep this item * as the first visible one even given that its index has been changed. */ - fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) { - Snapshot.withoutReadObservation { - update( - itemProvider.findIndexByKey(lastKnownFirstItemKey, index), - scrollOffset - ) - } + fun updateScrollPositionIfTheFirstItemWasMoved( + itemProvider: LazyGridItemProvider, + index: Int + ): Int { + val newIndex = itemProvider.findIndexByKey(lastKnownFirstItemKey, index) + this.index = newIndex + return newIndex } private fun update(index: Int, scrollOffset: Int) { require(index >= 0f) { "Index should be non-negative ($index)" } - if (index != this.index) { - this.index = index - } - if (scrollOffset != this.scrollOffset) { - this.scrollOffset = scrollOffset - } + this.index = index + this.scrollOffset = scrollOffset } } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt index 73e2a21b4f7..43920ef197d 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt @@ -38,6 +38,7 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.layout.Remeasurement import androidx.compose.ui.layout.RemeasurementModifier import androidx.compose.ui.unit.Constraints @@ -155,12 +156,12 @@ class LazyGridState constructor( /** * Needed for [animateScrollToItem]. Updated on every measure. */ - internal var density: Density by mutableStateOf(Density(1f, 1f)) + internal var density: Density = Density(1f, 1f) /** * Needed for [notifyPrefetch]. */ - internal var isVertical: Boolean by mutableStateOf(true) + internal var isVertical: Boolean = true /** * The ScrollableController instance. We keep it as we need to call stopAnimation on it once @@ -202,7 +203,7 @@ class LazyGridState constructor( * The [Remeasurement] object associated with our layout. It allows us to remeasure * synchronously during scroll. */ - internal var remeasurement: Remeasurement? by mutableStateOf(null) + internal var remeasurement: Remeasurement? = null /** * The modifier which provides [remeasurement]. @@ -428,9 +429,10 @@ class LazyGridState constructor( * items added or removed before our current first visible item and keep this item * as the first visible one even given that its index has been changed. */ - internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) { - scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider) - } + internal fun updateScrollPositionIfTheFirstItemWasMoved( + itemProvider: LazyGridItemProvider, + firstItemIndex: Int = Snapshot.withoutReadObservation { scrollPosition.index } + ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) companion object { /** diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt index f9dec0d82b5..4c368126f1c 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt @@ -41,8 +41,8 @@ internal class LazyStaggeredGridItemPlacementAnimator { // stored to not allocate it every pass. private val movingAwayKeys = LinkedHashSet<Any>() - private val movingInFromStartBound = mutableListOf<LazyStaggeredGridPositionedItem>() - private val movingInFromEndBound = mutableListOf<LazyStaggeredGridPositionedItem>() + private val movingInFromStartBound = mutableListOf<LazyStaggeredGridMeasuredItem>() + private val movingInFromEndBound = mutableListOf<LazyStaggeredGridMeasuredItem>() private val movingAwayToStartBound = mutableListOf<LazyStaggeredGridMeasuredItem>() private val movingAwayToEndBound = mutableListOf<LazyStaggeredGridMeasuredItem>() @@ -55,7 +55,7 @@ internal class LazyStaggeredGridItemPlacementAnimator { consumedScroll: Int, layoutWidth: Int, layoutHeight: Int, - positionedItems: MutableList<LazyStaggeredGridPositionedItem>, + positionedItems: MutableList<LazyStaggeredGridMeasuredItem>, itemProvider: LazyStaggeredGridMeasureProvider, isVertical: Boolean, laneCount: Int @@ -185,10 +185,9 @@ internal class LazyStaggeredGridItemPlacementAnimator { val mainAxisOffset = 0 - accumulatedOffsetPerLane[item.lane] val itemInfo = keyToItemInfoMap.getValue(item.key) - val positionedItem = - item.position(mainAxisOffset, itemInfo.crossAxisOffset, mainAxisLayoutSize) - positionedItems.add(positionedItem) - startAnimationsIfNeeded(positionedItem) + item.position(mainAxisOffset, itemInfo.crossAxisOffset, mainAxisLayoutSize) + positionedItems.add(item) + startAnimationsIfNeeded(item) } accumulatedOffsetPerLane.fill(0) } @@ -199,10 +198,9 @@ internal class LazyStaggeredGridItemPlacementAnimator { accumulatedOffsetPerLane[item.lane] += item.mainAxisSize val itemInfo = keyToItemInfoMap.getValue(item.key) - val positionedItem = - item.position(mainAxisOffset, itemInfo.crossAxisOffset, mainAxisLayoutSize) - positionedItems.add(positionedItem) - startAnimationsIfNeeded(positionedItem) + item.position(mainAxisOffset, itemInfo.crossAxisOffset, mainAxisLayoutSize) + positionedItems.add(item) + startAnimationsIfNeeded(item) } } @@ -224,7 +222,7 @@ internal class LazyStaggeredGridItemPlacementAnimator { } private fun initializeNode( - item: LazyStaggeredGridPositionedItem, + item: LazyStaggeredGridMeasuredItem, mainAxisOffset: Int ) { val firstPlaceableOffset = item.offset @@ -243,7 +241,7 @@ internal class LazyStaggeredGridItemPlacementAnimator { } } - private fun startAnimationsIfNeeded(item: LazyStaggeredGridPositionedItem) { + private fun startAnimationsIfNeeded(item: LazyStaggeredGridMeasuredItem) { item.forEachNode { node -> val newTarget = item.offset val currentTarget = node.rawOffset @@ -258,13 +256,13 @@ internal class LazyStaggeredGridItemPlacementAnimator { private val Any?.node get() = this as? LazyLayoutAnimateItemModifierNode - private val LazyStaggeredGridPositionedItem.hasAnimations: Boolean + private val LazyStaggeredGridMeasuredItem.hasAnimations: Boolean get() { forEachNode { return true } return false } - private inline fun LazyStaggeredGridPositionedItem.forEachNode( + private inline fun LazyStaggeredGridMeasuredItem.forEachNode( block: (LazyLayoutAnimateItemModifierNode) -> Unit ) { repeat(placeablesCount) { index -> diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt index 460d75afff0..79ad9cdcf7b 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt @@ -109,7 +109,12 @@ internal fun LazyLayoutMeasureScope.measureStaggeredGrid( val initialItemOffsets: IntArray Snapshot.withoutReadObservation { - val firstVisibleIndices = state.scrollPosition.indices + // ensure scroll position is up to date + val firstVisibleIndices = + state.updateScrollPositionIfTheFirstItemWasMoved( + itemProvider, + state.scrollPosition.indices + ) val firstVisibleOffsets = state.scrollPosition.offsets initialItemIndices = @@ -181,13 +186,20 @@ internal class LazyStaggeredGridMeasureContext( val reverseLayout: Boolean, val mainAxisSpacing: Int, ) { - val measuredItemProvider = LazyStaggeredGridMeasureProvider( + val measuredItemProvider = object : LazyStaggeredGridMeasureProvider( isVertical = isVertical, itemProvider = itemProvider, measureScope = measureScope, resolvedSlots = resolvedSlots, - ) { index, lane, span, key, contentType, placeables -> - LazyStaggeredGridMeasuredItem( + ) { + override fun createItem( + index: Int, + lane: Int, + span: Int, + key: Any, + contentType: Any?, + placeables: List<Placeable> + ) = LazyStaggeredGridMeasuredItem( index = index, key = key, placeables = placeables, @@ -751,13 +763,12 @@ private fun LazyStaggeredGridMeasureContext.measure( extraItemOffset = itemScrollOffsets[0] val extraItemsAfter = calculateExtraItems( position = { - val positionedItem = it.position( + it.position( mainAxis = extraItemOffset, crossAxis = 0, mainAxisLayoutSize = mainAxisLayoutSize ) extraItemOffset += it.sizeWithSpacings - positionedItem }, filter = { itemIndex -> if (itemIndex >= itemCount) { @@ -775,7 +786,7 @@ private fun LazyStaggeredGridMeasureContext.measure( } ) - val positionedItems = mutableListOf<LazyStaggeredGridPositionedItem>() + val positionedItems = mutableListOf<LazyStaggeredGridMeasuredItem>() positionedItems.addAll(extraItemsBefore) positionedItems.addAll(visibleItems) positionedItems.addAll(extraItemsAfter) @@ -830,8 +841,8 @@ private fun LazyStaggeredGridMeasureContext.calculateVisibleItems( measuredItems: Array<ArrayDeque<LazyStaggeredGridMeasuredItem>>, itemScrollOffsets: IntArray, mainAxisLayoutSize: Int, -): List<LazyStaggeredGridPositionedItem> { - val positionedItems = ArrayList<LazyStaggeredGridPositionedItem>( +): List<LazyStaggeredGridMeasuredItem> { + val positionedItems = ArrayList<LazyStaggeredGridMeasuredItem>( measuredItems.sumOf { it.size } ) while (measuredItems.any { it.isNotEmpty() }) { @@ -853,14 +864,12 @@ private fun LazyStaggeredGridMeasureContext.calculateVisibleItems( // nothing to place, ignore spacings continue } - - positionedItems += - item.position( - mainAxis = mainAxisOffset, - crossAxis = crossAxisOffset, - mainAxisLayoutSize = mainAxisLayoutSize, - lane = laneIndex - ) + item.position( + mainAxis = mainAxisOffset, + crossAxis = crossAxisOffset, + mainAxisLayoutSize = mainAxisLayoutSize, + ) + positionedItems += item spanRange.forEach { lane -> itemScrollOffsets[lane] = mainAxisOffset + item.sizeWithSpacings } @@ -870,10 +879,10 @@ private fun LazyStaggeredGridMeasureContext.calculateVisibleItems( @ExperimentalFoundationApi private inline fun LazyStaggeredGridMeasureContext.calculateExtraItems( - position: (LazyStaggeredGridMeasuredItem) -> LazyStaggeredGridPositionedItem, + position: (LazyStaggeredGridMeasuredItem) -> Unit, filter: (itemIndex: Int) -> Boolean -): List<LazyStaggeredGridPositionedItem> { - var result: MutableList<LazyStaggeredGridPositionedItem>? = null +): List<LazyStaggeredGridMeasuredItem> { + var result: MutableList<LazyStaggeredGridMeasuredItem>? = null pinnedItems.fastForEach { index -> if (filter(index)) { @@ -882,7 +891,8 @@ private inline fun LazyStaggeredGridMeasureContext.calculateExtraItems( result = mutableListOf() } val measuredItem = measuredItemProvider.getAndMeasure(index, spanRange) - result?.add(position(measuredItem)) + position(measuredItem) + result?.add(measuredItem) } } @@ -987,12 +997,11 @@ private fun LazyStaggeredGridMeasureContext.findPreviousItemIndex(item: Int, lan laneInfo.findPreviousItemIndex(item, lane) @OptIn(ExperimentalFoundationApi::class) -internal class LazyStaggeredGridMeasureProvider( +internal abstract class LazyStaggeredGridMeasureProvider( private val isVertical: Boolean, private val itemProvider: LazyStaggeredGridItemProvider, private val measureScope: LazyLayoutMeasureScope, - private val resolvedSlots: LazyStaggeredGridSlots, - private val measuredItemFactory: MeasuredItemFactory, + private val resolvedSlots: LazyStaggeredGridSlots ) { private fun childConstraints(requestedSlot: Int, requestedSpan: Int): Constraints { val slotCount = resolvedSlots.sizes.size @@ -1020,7 +1029,7 @@ internal class LazyStaggeredGridMeasureProvider( val key = keyIndexMap.getKey(index) ?: itemProvider.getKey(index) val contentType = itemProvider.getContentType(index) val placeables = measureScope.measure(index, childConstraints(span.start, span.size)) - return measuredItemFactory.createItem( + return createItem( index, span.start, span.size, @@ -1031,11 +1040,8 @@ internal class LazyStaggeredGridMeasureProvider( } val keyIndexMap: LazyLayoutKeyIndexMap = itemProvider.keyIndexMap -} -// This interface allows to avoid autoboxing on index param -internal fun interface MeasuredItemFactory { - fun createItem( + abstract fun createItem( index: Int, lane: Int, span: Int, @@ -1046,17 +1052,17 @@ internal fun interface MeasuredItemFactory { } internal class LazyStaggeredGridMeasuredItem( - val index: Int, - val key: Any, + override val index: Int, + override val key: Any, private val placeables: List<Placeable>, - private val isVertical: Boolean, + val isVertical: Boolean, spacing: Int, - val lane: Int, + override val lane: Int, val span: Int, private val beforeContentPadding: Int, private val afterContentPadding: Int, - private val contentType: Any? -) { + override val contentType: Any? +) : LazyStaggeredGridItemInfo { var isVisible = true val placeablesCount: Int get() = placeables.size @@ -1073,63 +1079,40 @@ internal class LazyStaggeredGridMeasuredItem( if (isVertical) it.width else it.height } ?: 0 + private var mainAxisLayoutSize: Int = Unset + private var minMainAxisOffset: Int = 0 + private var maxMainAxisOffset: Int = 0 + + override val size: IntSize = if (isVertical) { + IntSize(crossAxisSize, mainAxisSize) + } else { + IntSize(mainAxisSize, crossAxisSize) + } + override var offset: IntOffset = IntOffset.Zero + private set + fun position( mainAxis: Int, crossAxis: Int, mainAxisLayoutSize: Int, - lane: Int = 0 - ): LazyStaggeredGridPositionedItem = - LazyStaggeredGridPositionedItem( - offset = if (isVertical) { - IntOffset(crossAxis, mainAxis) - } else { - IntOffset(mainAxis, crossAxis) - }, - lane = lane, - index = index, - key = key, - size = if (isVertical) { - IntSize(crossAxisSize, mainAxisSize) - } else { - IntSize(mainAxisSize, crossAxisSize) - }, - placeables = placeables, - isVertical = isVertical, - mainAxisLayoutSize = mainAxisLayoutSize, - minMainAxisOffset = -beforeContentPadding, - maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding, - span = span, - contentType = contentType - ) -} - -internal class LazyStaggeredGridPositionedItem( - override val offset: IntOffset, - override val index: Int, - override val lane: Int, - override val key: Any, - override val size: IntSize, - private val placeables: List<Placeable>, - val isVertical: Boolean, - private val mainAxisLayoutSize: Int, - private val minMainAxisOffset: Int, - private val maxMainAxisOffset: Int, - val span: Int, - override val contentType: Any? -) : LazyStaggeredGridItemInfo { - - val placeablesCount: Int get() = placeables.size - - val mainAxisSize get() = if (isVertical) size.height else size.width + ) { + this.mainAxisLayoutSize = mainAxisLayoutSize + minMainAxisOffset = -beforeContentPadding + maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding + offset = if (isVertical) { + IntOffset(crossAxis, mainAxis) + } else { + IntOffset(mainAxis, crossAxis) + } + } val crossAxisOffset get() = if (isVertical) offset.x else offset.y - fun getParentData(index: Int) = placeables[index].parentData - fun place( scope: Placeable.PlacementScope, context: LazyStaggeredGridMeasureContext ) = with(context) { + require(mainAxisLayoutSize != Unset) { "position() should be called first" } with(scope) { placeables.fastForEachIndexed { index, placeable -> val minOffset = minMainAxisOffset - placeable.mainAxisSize @@ -1170,3 +1153,5 @@ internal class LazyStaggeredGridPositionedItem( super.toString() } } + +private const val Unset = Int.MIN_VALUE diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt index c72c1145bcb..b6d7fae7361 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt @@ -68,9 +68,6 @@ internal fun rememberStaggeredGridMeasurePolicy( state.isVertical = isVertical state.spanProvider = itemProvider.spanProvider - // ensure scroll position is up to date - state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider) - // setup measure val beforeContentPadding = contentPadding.beforePadding( orientation, reverseLayout, layoutDirection diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt index 5d5fe3306f4..6f418011f9c 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt @@ -19,6 +19,7 @@ package androidx.compose.foundation.lazy.staggeredgrid import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider import androidx.compose.foundation.lazy.layout.findIndexByKey +import androidx.compose.runtime.SnapshotMutationPolicy import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -30,9 +31,11 @@ internal class LazyStaggeredGridScrollPosition( initialIndices: IntArray, initialOffsets: IntArray, private val fillIndices: (targetIndex: Int, laneCount: Int) -> IntArray -) { - var indices by mutableStateOf(initialIndices) - var offsets by mutableStateOf(initialOffsets) +) : SnapshotMutationPolicy<IntArray> { + var indices by mutableStateOf(initialIndices, this) + private set + var offsets by mutableStateOf(initialOffsets, this) + private set private var hadFirstNotEmptyLayout = false @@ -91,27 +94,28 @@ internal class LazyStaggeredGridScrollPosition( * as the first visible one even given that its index has been changed. */ @ExperimentalFoundationApi - fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyLayoutItemProvider) { - Snapshot.withoutReadObservation { - val lastIndex = itemProvider.findIndexByKey( - key = lastKnownFirstItemKey, - lastKnownIndex = indices.getOrNull(0) ?: 0 - ) - if (lastIndex !in indices) { - update( - fillIndices(lastIndex, indices.size), - offsets - ) - } + fun updateScrollPositionIfTheFirstItemWasMoved( + itemProvider: LazyLayoutItemProvider, + indices: IntArray + ): IntArray { + val lastIndex = itemProvider.findIndexByKey( + key = lastKnownFirstItemKey, + lastKnownIndex = indices.getOrNull(0) ?: 0 + ) + return if (lastIndex !in indices) { + val newIndices = fillIndices(lastIndex, indices.size) + this.indices = newIndices + newIndices + } else { + indices } } private fun update(indices: IntArray, offsets: IntArray) { - if (!indices.contentEquals(this.indices)) { - this.indices = indices - } - if (!offsets.contentEquals(this.offsets)) { - this.offsets = offsets - } + this.indices = indices + this.offsets = offsets } + + // mutation policy for int arrays + override fun equivalent(a: IntArray, b: IntArray) = a.contentEquals(b) }
\ No newline at end of file diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt index 84f9df6a64c..1c6233d8e89 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt @@ -39,6 +39,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.ui.layout.Remeasurement import androidx.compose.ui.layout.RemeasurementModifier @@ -331,9 +332,11 @@ class LazyStaggeredGridState private constructor( /** * Maintain scroll position for item based on custom key if its index has changed. */ - internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyLayoutItemProvider) { - scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider) - } + internal fun updateScrollPositionIfTheFirstItemWasMoved( + itemProvider: LazyLayoutItemProvider, + firstItemIndex: IntArray = Snapshot.withoutReadObservation { scrollPosition.indices } + ): IntArray = + scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta) diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt index 8543acdf57f..8ad0a3ec072 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt @@ -16,86 +16,103 @@ package androidx.compose.foundation.pager +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.Alignment import androidx.compose.ui.layout.Placeable import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed +@OptIn(ExperimentalFoundationApi::class) internal class MeasuredPage( - val index: Int, + override val index: Int, val size: Int, - val placeables: List<Placeable>, - val visualOffset: IntOffset, + private val placeables: List<Placeable>, + private val visualOffset: IntOffset, val key: Any, - val orientation: Orientation, - val horizontalAlignment: Alignment.Horizontal?, - val verticalAlignment: Alignment.Vertical?, - val layoutDirection: LayoutDirection, - val reverseLayout: Boolean, - val beforeContentPadding: Int, - val afterContentPadding: Int, -) { + orientation: Orientation, + private val horizontalAlignment: Alignment.Horizontal?, + private val verticalAlignment: Alignment.Vertical?, + private val layoutDirection: LayoutDirection, + private val reverseLayout: Boolean +) : PageInfo { + + private val isVertical = orientation == Orientation.Vertical val crossAxisSize: Int + // optimized for storing x and y offsets for each placeable one by one. + // array's size == placeables.size * 2, first we store x, then y. + private val placeableOffsets: IntArray + init { var maxCrossAxis = 0 placeables.fastForEach { maxCrossAxis = maxOf( maxCrossAxis, - if (orientation != Orientation.Vertical) it.height else it.width + if (!isVertical) it.height else it.width ) } crossAxisSize = maxCrossAxis + placeableOffsets = IntArray(placeables.size * 2) } + override var offset: Int = 0 + private set + + private var mainAxisLayoutSize: Int = Unset + fun position( offset: Int, layoutWidth: Int, layoutHeight: Int - ): PositionedPage { - val wrappers = mutableListOf<PagerPlaceableWrapper>() - val mainAxisLayoutSize = - if (orientation == Orientation.Vertical) layoutHeight else layoutWidth - var mainAxisOffset = if (reverseLayout) { - mainAxisLayoutSize - offset - size - } else { - offset + ) { + this.offset = offset + mainAxisLayoutSize = + if (isVertical) layoutHeight else layoutWidth + var mainAxisOffset = offset + placeables.fastForEachIndexed { index, placeable -> + val indexInArray = index * 2 + if (isVertical) { + placeableOffsets[indexInArray] = requireNotNull(horizontalAlignment) + .align(placeable.width, layoutWidth, layoutDirection) + placeableOffsets[indexInArray + 1] = mainAxisOffset + mainAxisOffset += placeable.height + } else { + placeableOffsets[indexInArray] = mainAxisOffset + placeableOffsets[indexInArray + 1] = requireNotNull(verticalAlignment) + .align(placeable.height, layoutHeight) + mainAxisOffset += placeable.width + } } - var index = if (reverseLayout) placeables.lastIndex else 0 - while (if (reverseLayout) index >= 0 else index < placeables.size) { - val it = placeables[index] - val addIndex = if (reverseLayout) 0 else wrappers.size - val placeableOffset = if (orientation == Orientation.Vertical) { - val x = requireNotNull(horizontalAlignment) - .align(it.width, layoutWidth, layoutDirection) - IntOffset(x, mainAxisOffset) + } + + fun place(scope: Placeable.PlacementScope) = with(scope) { + require(mainAxisLayoutSize != Unset) { "position() should be called first" } + repeat(placeables.size) { index -> + val placeable = placeables[index] + var offset = getOffset(index) + if (reverseLayout) { + offset = offset.copy { mainAxisOffset -> + mainAxisLayoutSize - mainAxisOffset - placeable.mainAxisSize + } + } + offset += visualOffset + if (isVertical) { + placeable.placeWithLayer(offset) } else { - val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight) - IntOffset(mainAxisOffset, y) + placeable.placeRelativeWithLayer(offset) } - mainAxisOffset += if (orientation == Orientation.Vertical) it.height else it.width - wrappers.add( - addIndex, - PagerPlaceableWrapper(placeableOffset, it, placeables[index].parentData) - ) - if (reverseLayout) index-- else index++ } - return PositionedPage( - offset = offset, - index = this.index, - key = key, - orientation = orientation, - wrappers = wrappers, - visualOffset = visualOffset, - ) } + + private fun getOffset(index: Int) = + IntOffset(placeableOffsets[index * 2], placeableOffsets[index * 2 + 1]) + private val Placeable.mainAxisSize get() = if (isVertical) height else width + private inline fun IntOffset.copy(mainAxisMap: (Int) -> Int): IntOffset = + IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y) } -internal class PagerPlaceableWrapper( - val offset: IntOffset, - val placeable: Placeable, - val parentData: Any? -)
\ No newline at end of file +private const val Unset = Int.MIN_VALUE diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt index 4c6eb096ab9..90e0f3db618 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt @@ -119,7 +119,7 @@ internal fun LazyLayoutMeasureScope.measurePager( } // this will contain all the measured pages representing the visible pages - val visiblePages = mutableListOf<MeasuredPage>() + val visiblePages = ArrayDeque<MeasuredPage>() // define min and max offsets val minOffset = -beforeContentPadding + if (spaceBetweenPages < 0) spaceBetweenPages else 0 @@ -146,8 +146,6 @@ internal fun LazyLayoutMeasureScope.measurePager( orientation = orientation, horizontalAlignment = horizontalAlignment, verticalAlignment = verticalAlignment, - afterContentPadding = afterContentPadding, - beforeContentPadding = beforeContentPadding, layoutDirection = layoutDirection, reverseLayout = reverseLayout, pageAvailableSize = pageAvailableSize @@ -194,8 +192,6 @@ internal fun LazyLayoutMeasureScope.measurePager( orientation = orientation, horizontalAlignment = horizontalAlignment, verticalAlignment = verticalAlignment, - afterContentPadding = afterContentPadding, - beforeContentPadding = beforeContentPadding, layoutDirection = layoutDirection, reverseLayout = reverseLayout, pageAvailableSize = pageAvailableSize @@ -232,8 +228,6 @@ internal fun LazyLayoutMeasureScope.measurePager( orientation = orientation, horizontalAlignment = horizontalAlignment, verticalAlignment = verticalAlignment, - afterContentPadding = afterContentPadding, - beforeContentPadding = beforeContentPadding, layoutDirection = layoutDirection, reverseLayout = reverseLayout, pageAvailableSize = pageAvailableSize @@ -298,8 +292,6 @@ internal fun LazyLayoutMeasureScope.measurePager( orientation = orientation, horizontalAlignment = horizontalAlignment, verticalAlignment = verticalAlignment, - afterContentPadding = afterContentPadding, - beforeContentPadding = beforeContentPadding, layoutDirection = layoutDirection, reverseLayout = reverseLayout, pageAvailableSize = pageAvailableSize @@ -326,8 +318,6 @@ internal fun LazyLayoutMeasureScope.measurePager( orientation = orientation, horizontalAlignment = horizontalAlignment, verticalAlignment = verticalAlignment, - afterContentPadding = afterContentPadding, - beforeContentPadding = beforeContentPadding, layoutDirection = layoutDirection, reverseLayout = reverseLayout, pageAvailableSize = pageAvailableSize @@ -478,8 +468,6 @@ private fun LazyLayoutMeasureScope.getAndMeasure( orientation: Orientation, horizontalAlignment: Alignment.Horizontal?, verticalAlignment: Alignment.Vertical?, - afterContentPadding: Int, - beforeContentPadding: Int, layoutDirection: LayoutDirection, reverseLayout: Boolean, pageAvailableSize: Int @@ -493,8 +481,6 @@ private fun LazyLayoutMeasureScope.getAndMeasure( visualOffset = visualPageOffset, horizontalAlignment = horizontalAlignment, verticalAlignment = verticalAlignment, - afterContentPadding = afterContentPadding, - beforeContentPadding = beforeContentPadding, layoutDirection = layoutDirection, reverseLayout = reverseLayout, size = pageAvailableSize, @@ -518,7 +504,7 @@ private fun LazyLayoutMeasureScope.calculatePagesOffsets( density: Density, spaceBetweenPages: Int, pageAvailableSize: Int -): MutableList<PositionedPage> { +): MutableList<MeasuredPage> { val pageSizeWithSpacing = (pageAvailableSize + spaceBetweenPages) val mainAxisLayoutSize = if (orientation == Orientation.Vertical) layoutHeight else layoutWidth val hasSpareSpace = finalMainAxisOffset < minOf(mainAxisLayoutSize, maxOffset) @@ -526,7 +512,7 @@ private fun LazyLayoutMeasureScope.calculatePagesOffsets( check(pagesScrollOffset == 0) } val positionedPages = - ArrayList<PositionedPage>(pages.size + extraPagesBefore.size + extraPagesAfter.size) + ArrayList<MeasuredPage>(pages.size + extraPagesBefore.size + extraPagesAfter.size) if (hasSpareSpace) { require(extraPagesBefore.isEmpty() && extraPagesAfter.isEmpty()) @@ -560,23 +546,27 @@ private fun LazyLayoutMeasureScope.calculatePagesOffsets( } else { absoluteOffset } - positionedPages.add(page.position(relativeOffset, layoutWidth, layoutHeight)) + page.position(relativeOffset, layoutWidth, layoutHeight) + positionedPages.add(page) } } else { var currentMainAxis = pagesScrollOffset extraPagesBefore.fastForEach { currentMainAxis -= pageSizeWithSpacing - positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight)) + it.position(currentMainAxis, layoutWidth, layoutHeight) + positionedPages.add(it) } currentMainAxis = pagesScrollOffset pages.fastForEach { - positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight)) + it.position(currentMainAxis, layoutWidth, layoutHeight) + positionedPages.add(it) currentMainAxis += pageSizeWithSpacing } extraPagesAfter.fastForEach { - positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight)) + it.position(currentMainAxis, layoutWidth, layoutHeight) + positionedPages.add(it) currentMainAxis += pageSizeWithSpacing } } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt index cecb3717f04..500b396ef88 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.Snapshot /** * Contains the current scroll position represented by the first visible page and the first @@ -55,15 +54,13 @@ internal class PagerScrollPosition( val scrollOffset = measureResult.firstVisiblePageOffset check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" } - Snapshot.withoutReadObservation { - update( - measureResult.firstVisiblePage?.index ?: 0, - scrollOffset - ) - measureResult.closestPageToSnapPosition?.index?.let { - if (it != this.currentPage) { - this.currentPage = it - } + update( + measureResult.firstVisiblePage?.index ?: 0, + scrollOffset + ) + measureResult.closestPageToSnapPosition?.index?.let { + if (it != this.currentPage) { + this.currentPage = it } } } @@ -89,11 +86,7 @@ internal class PagerScrollPosition( private fun update(index: Int, scrollOffset: Int) { require(index >= 0f) { "Index should be non-negative ($index)" } - if (index != this.firstVisiblePage) { - this.firstVisiblePage = index - } - if (scrollOffset != this.scrollOffset) { - this.scrollOffset = scrollOffset - } + this.firstVisiblePage = index + this.scrollOffset = scrollOffset } }
\ No newline at end of file diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt index ea799c26ccf..fd85de16aa4 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt @@ -240,7 +240,7 @@ abstract class PagerState( internal val pageSize: Int get() = pagerLayoutInfoState.value.pageSize - internal var density: Density by mutableStateOf(UnitDensity) + internal var density: Density = UnitDensity private val visiblePages: List<PageInfo> get() = pagerLayoutInfoState.value.visiblePagesInfo @@ -397,7 +397,7 @@ abstract class PagerState( /** * Constraints passed to the prefetcher for premeasuring the prefetched items. */ - internal var premeasureConstraints by mutableStateOf(Constraints()) + internal var premeasureConstraints = Constraints() /** * Stores currently pinned pages which are always composed, used by for beyond bound pages. diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PositionedPage.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PositionedPage.kt deleted file mode 100644 index 57ce77f2c02..00000000000 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PositionedPage.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023 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.pager - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.layout.Placeable -import androidx.compose.ui.unit.IntOffset - -@OptIn(ExperimentalFoundationApi::class) -internal class PositionedPage( - override val index: Int, - override val offset: Int, - val key: Any, - val orientation: Orientation, - val wrappers: MutableList<PagerPlaceableWrapper>, - val visualOffset: IntOffset -) : PageInfo { - fun place(scope: Placeable.PlacementScope) = with(scope) { - repeat(wrappers.size) { index -> - val placeable = wrappers[index].placeable - val offset = wrappers[index].offset - if (orientation == Orientation.Vertical) { - placeable.placeWithLayer(offset + visualOffset) - } else { - placeable.placeRelativeWithLayer(offset + visualOffset) - } - } - } -}
\ No newline at end of file |