diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-06 22:03:08 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2023-12-06 22:03:08 +0000 |
commit | 10035bb95102a1761733ddc6ded07d3963bc307b (patch) | |
tree | e382ee60aae3af5bf282bf81deae8de49240ea3d | |
parent | 224977ad5d1e9eb74a78a3fbe81ec2f27bf2ff6c (diff) | |
parent | 25adc5acf10067b8a8ab04b28c6476e0b4e275e4 (diff) | |
download | support-10035bb95102a1761733ddc6ded07d3963bc307b.tar.gz |
Merge "Only do relayout when pager scrolling doesn't require composing/disposing" into snap-temp-L11100030000693181
9 files changed, 293 insertions, 44 deletions
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt index bce771a8b23..7f45c8a2e67 100644 --- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt +++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.test.swipeWithVelocity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import kotlin.math.absoluteValue import kotlin.test.assertTrue import kotlinx.coroutines.CoroutineScope @@ -351,12 +352,12 @@ open class BasePagerTest(private val config: ParamConfig) : pagerState.currentPageOffsetFraction != 0.0f } // wait for first move from drag rule.mainClock.advanceTimeUntil { - pagerState.currentPageOffsetFraction == 0.0f + pagerState.currentPageOffsetFraction.absoluteValue < 0.00001 } // wait for fling settling // pump the clock twice and check we're still settled. rule.mainClock.advanceTimeByFrame() rule.mainClock.advanceTimeByFrame() - assertTrue { pagerState.currentPageOffsetFraction == 0.0f } + assertTrue { pagerState.currentPageOffsetFraction.absoluteValue < 0.00001 } } } diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt index 69ef7443219..71635fb0a8b 100644 --- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt +++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt @@ -89,10 +89,10 @@ class PageLayoutPositionOnScrollingTest( add( ParamConfig( orientation = orientation, - pageSpacing = pageSpacing, mainAxisContentPadding = contentPadding, reverseLayout = reverseLayout, - layoutDirection = layoutDirection + layoutDirection = layoutDirection, + pageSpacing = pageSpacing ) ) } diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt index a625dc95ae9..01661b9b3bf 100644 --- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt +++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt @@ -18,6 +18,7 @@ package androidx.compose.foundation.pager import androidx.compose.foundation.AutoTestFrameClock import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size @@ -35,6 +36,8 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.test.filters.LargeTest import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat +import kotlin.math.roundToInt import kotlin.test.assertFalse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -413,6 +416,104 @@ class PagerStateNonGestureScrollingTest(val config: ParamConfig) : BasePagerTest Truth.assertThat(pagerState.targetPage).isEqualTo(pagerState.currentPage) } + @Test + fun canScrollForwardAndBackward_afterSmallScrollFromStart() { + val pageSizePx = 100 + val pageSizeDp = with(rule.density) { pageSizePx.toDp() } + createPager( + modifier = Modifier.size(pageSizeDp * 1.5f), + pageSize = { PageSize.Fixed(pageSizeDp) }) + + val delta = (pageSizePx / 3f).roundToInt() + + runBlocking { + withContext(Dispatchers.Main + AutoTestFrameClock()) { + // small enough scroll to not cause any new items to be composed or old ones disposed. + pagerState.scrollBy(delta.toFloat()) + } + rule.runOnIdle { + assertThat(pagerState.firstVisiblePageOffset).isEqualTo(delta) + assertThat(pagerState.canScrollForward).isTrue() + assertThat(pagerState.canScrollBackward).isTrue() + } + // and scroll back to start + withContext(Dispatchers.Main + AutoTestFrameClock()) { + pagerState.scrollBy(-delta.toFloat()) + } + rule.runOnIdle { + assertThat(pagerState.canScrollForward).isTrue() + assertThat(pagerState.canScrollBackward).isFalse() + } + } + } + + @Test + fun canScrollForwardAndBackward_afterSmallScrollFromEnd() { + val pageSizePx = 100 + val pageSizeDp = with(rule.density) { pageSizePx.toDp() } + createPager( + modifier = Modifier.size(pageSizeDp * 1.5f), + pageSize = { PageSize.Fixed(pageSizeDp) }) + val delta = -(pageSizePx / 3f).roundToInt() + runBlocking { + withContext(Dispatchers.Main + AutoTestFrameClock()) { + // scroll to the end of the list. + pagerState.scrollToPage(DefaultPageCount) + // small enough scroll to not cause any new items to be composed or old ones disposed. + pagerState.scrollBy(delta.toFloat()) + } + rule.runOnIdle { + assertThat(pagerState.canScrollForward).isTrue() + assertThat(pagerState.canScrollBackward).isTrue() + } + // and scroll back to the end + withContext(Dispatchers.Main + AutoTestFrameClock()) { + pagerState.scrollBy(-delta.toFloat()) + } + rule.runOnIdle { + assertThat(pagerState.canScrollForward).isFalse() + assertThat(pagerState.canScrollBackward).isTrue() + } + } + } + + @Test + fun canScrollForwardAndBackward_afterSmallScrollFromEnd_withContentPadding() { + val pageSizePx = 100 + val pageSizeDp = with(rule.density) { pageSizePx.toDp() } + val afterContentPaddingDp = with(rule.density) { 2.toDp() } + createPager( + modifier = Modifier.size(pageSizeDp * 1.5f), + pageSize = { PageSize.Fixed(pageSizeDp) }, + contentPadding = PaddingValues(afterContent = afterContentPaddingDp) + ) + + val delta = -(pageSizePx / 3f).roundToInt() + runBlocking { + withContext(Dispatchers.Main + AutoTestFrameClock()) { + // scroll to the end of the list. + pagerState.scrollToPage(DefaultPageCount) + + assertThat(pagerState.canScrollForward).isFalse() + assertThat(pagerState.canScrollBackward).isTrue() + + // small enough scroll to not cause any new pages to be composed or old ones disposed. + pagerState.scrollBy(delta.toFloat()) + } + rule.runOnIdle { + assertThat(pagerState.canScrollForward).isTrue() + assertThat(pagerState.canScrollBackward).isTrue() + } + // and scroll back to the end + withContext(Dispatchers.Main + AutoTestFrameClock()) { + pagerState.scrollBy(-delta.toFloat()) + } + rule.runOnIdle { + assertThat(pagerState.canScrollForward).isFalse() + assertThat(pagerState.canScrollBackward).isTrue() + } + } + } companion object { @JvmStatic @Parameterized.Parameters(name = "{0}") 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 45748d40c99..65864337995 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 @@ -78,14 +78,14 @@ internal class MeasuredPage( if (isVertical) { placeableOffsets[indexInArray] = requireNotNull(horizontalAlignment) { "null horizontalAlignment" } - .align(placeable.width, layoutWidth, layoutDirection) + .align(placeable.width, layoutWidth, layoutDirection) placeableOffsets[indexInArray + 1] = mainAxisOffset mainAxisOffset += placeable.height } else { placeableOffsets[indexInArray] = mainAxisOffset placeableOffsets[indexInArray + 1] = requireNotNull(verticalAlignment) { "null verticalAlignment" } - .align(placeable.height, layoutHeight) + .align(placeable.height, layoutHeight) mainAxisOffset += placeable.width } } @@ -110,8 +110,20 @@ internal class MeasuredPage( } } + fun applyScrollDelta(delta: Int) { + offset += delta + repeat(placeableOffsets.size) { index -> + // placeableOffsets consist of x and y pairs for each placeable. + // if isVertical is true then the main axis offsets are located at indexes 1, 3, 5 etc. + if ((isVertical && index % 2 == 1) || (!isVertical && index % 2 == 0)) { + placeableOffsets[index] += delta + } + } + } + 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) 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 e68a9904f5f..888825bd5be 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 @@ -22,6 +22,7 @@ import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope +import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator import androidx.compose.ui.Alignment import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.Placeable @@ -56,6 +57,7 @@ internal fun LazyLayoutMeasureScope.measurePager( beyondBoundsPageCount: Int, pinnedPages: List<Int>, snapPositionInLayout: SnapPositionInLayout, + placementScopeInvalidator: ObservableScopeInvalidator, layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult ): PagerMeasureResult { require(beforeContentPadding >= 0) { "negative beforeContentPadding" } @@ -81,7 +83,8 @@ internal fun LazyLayoutMeasureScope.measurePager( beyondBoundsPageCount = beyondBoundsPageCount, canScrollForward = false, currentPage = null, - currentPageOffsetFraction = 0.0f + currentPageOffsetFraction = 0.0f, + remeasureNeeded = false ) } else { @@ -169,10 +172,24 @@ internal fun LazyLayoutMeasureScope.measurePager( val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0) var currentMainAxisOffset = -currentFirstPageScrollOffset + // will be set to true if we composed some items only to know their size and apply scroll, + // while in the end this item will not end up in the visible viewport. we will need an + // extra remeasure in order to dispose such items. + var remeasureNeeded = false + // first we need to skip pages we already composed while composing backward - visiblePages.fastForEach { - index++ - currentMainAxisOffset += pageSizeWithSpacing + var indexInVisibleItems = 0 + + while (indexInVisibleItems < visiblePages.size) { + if (currentMainAxisOffset >= maxMainAxis) { + // this item is out of the bounds and will not be visible. + visiblePages.removeAt(indexInVisibleItems) + remeasureNeeded = true + } else { + index++ + currentMainAxisOffset += pageSizeWithSpacing + indexInVisibleItems++ + } } // then composing visible pages forward until we fill the whole viewport. @@ -204,9 +221,10 @@ internal fun LazyLayoutMeasureScope.measurePager( } if (currentMainAxisOffset <= minOffset && index != pageCount - 1) { - // this page is offscreen and will not be placed. advance firstVisiblePage + // this page is offscreen and will not be visible. advance currentFirstPage currentFirstPage = index + 1 currentFirstPageScrollOffset -= pageSizeWithSpacing + remeasureNeeded = true } else { maxCrossAxis = maxOf(maxCrossAxis, measuredPage.crossAxisSize) visiblePages.add(measuredPage) @@ -392,6 +410,8 @@ internal fun LazyLayoutMeasureScope.measurePager( positionedPages.fastForEach { it.place(this) } + // we attach it during the placement so PagerState can trigger re-placement + placementScopeInvalidator.attachToScope() }, viewportStartOffset = -beforeContentPadding, viewportEndOffset = maxOffset + afterContentPadding, @@ -404,7 +424,8 @@ internal fun LazyLayoutMeasureScope.measurePager( beyondBoundsPageCount = beyondBoundsPageCount, canScrollForward = index < pageCount || currentMainAxisOffset > maxOffset, currentPage = newCurrentPage, - currentPageOffsetFraction = newCurrentPageOffsetFraction + currentPageOffsetFraction = newCurrentPageOffsetFraction, + remeasureNeeded = remeasureNeeded ) } } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt index 6f30f0f5f1e..90eb4e40885 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt @@ -175,8 +175,8 @@ internal fun rememberPagerMeasurePolicy( reverseLayout = reverseLayout, pinnedPages = pinnedPages, snapPositionInLayout = snapPositionInLayout, + placementScopeInvalidator = state.placementScopeInvalidator, layout = { width, height, placement -> - state.remeasureTrigger // read state to trigger remeasures on state write layout( containerConstraints.constrainWidth(width + totalHorizontalPadding), containerConstraints.constrainHeight(height + totalVerticalPadding), diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt index 16832df0079..1c09f91a069 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt @@ -20,10 +20,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.util.fastForEach @OptIn(ExperimentalFoundationApi::class) internal class PagerMeasureResult( - override val visiblePagesInfo: List<PageInfo>, + override val visiblePagesInfo: List<MeasuredPage>, override val pageSize: Int, override val pageSpacing: Int, override val afterContentPadding: Int, @@ -34,12 +35,85 @@ internal class PagerMeasureResult( override val beyondBoundsPageCount: Int, val firstVisiblePage: MeasuredPage?, val currentPage: MeasuredPage?, - val firstVisiblePageScrollOffset: Int, - val currentPageOffsetFraction: Float, - val canScrollForward: Boolean, + var currentPageOffsetFraction: Float, + var firstVisiblePageScrollOffset: Int, + var canScrollForward: Boolean, measureResult: MeasureResult, + /** True when extra remeasure is required. */ + val remeasureNeeded: Boolean, ) : PagerLayoutInfo, MeasureResult by measureResult { override val viewportSize: IntSize get() = IntSize(width, height) override val beforeContentPadding: Int get() = -viewportStartOffset + + val canScrollBackward + get() = (firstVisiblePage?.index ?: 0) != 0 || firstVisiblePageScrollOffset != 0 + + /** + * Tries to apply a scroll [delta] for this layout info. In some cases we can apply small + * scroll deltas by just changing the offsets for each [visiblePagesInfo]. + * But we can only do so if after applying the delta we would not need to compose a new item + * or dispose an item which is currently visible. In this case this function will not apply + * the [delta] and return false. + * + * @return true if we can safely apply a passed scroll [delta] to this layout info. + * If true is returned, only the placement phase is needed to apply new offsets. + * If false is returned, it means we have to rerun the full measure phase to apply the [delta]. + */ + fun tryToApplyScrollWithoutRemeasure(delta: Int): Boolean { + val pageSizeWithSpacing = pageSize + pageSpacing + if (remeasureNeeded || visiblePagesInfo.isEmpty() || firstVisiblePage == null || + // applying this delta will change firstVisibleItem + (firstVisiblePageScrollOffset - delta) !in 0 until pageSizeWithSpacing + ) { + return false + } + + val deltaFraction = if (pageSizeWithSpacing != 0) { + (delta / pageSizeWithSpacing.toFloat()) + } else { + 0.0f + } + + val newCurrentPageOffsetFraction = currentPageOffsetFraction - deltaFraction + if (currentPage == null || + // applying this delta will change current page + newCurrentPageOffsetFraction >= MaxPageOffset || + newCurrentPageOffsetFraction <= MinPageOffset + ) { + return false + } + + val first = visiblePagesInfo.first() + val last = visiblePagesInfo.last() + val canApply = if (delta < 0) { + // scrolling forward + val deltaToFirstItemChange = + first.offset + pageSizeWithSpacing - viewportStartOffset + val deltaToLastItemChange = + last.offset + pageSizeWithSpacing - viewportEndOffset + minOf(deltaToFirstItemChange, deltaToLastItemChange) > -delta + } else { + // scrolling backward + val deltaToFirstItemChange = + viewportStartOffset - first.offset + val deltaToLastItemChange = + viewportEndOffset - last.offset + minOf(deltaToFirstItemChange, deltaToLastItemChange) > delta + } + return if (canApply) { + currentPageOffsetFraction -= deltaFraction + firstVisiblePageScrollOffset -= delta + visiblePagesInfo.fastForEach { + it.applyScrollDelta(delta) + } + if (!canScrollForward && delta > 0) { + // we scrolled backward, so now we can scroll forward + canScrollForward = true + } + true + } else { + false + } + } } 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 0a8d2ba10d8..0ea47f9e1a3 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 @@ -112,6 +112,10 @@ internal class PagerScrollPosition( } } + fun updateCurrentPageOffsetFraction(offsetFraction: Float) { + currentPageOffsetFraction = offsetFraction + } + fun currentScrollOffset(): Int { return ((currentPage + currentPageOffsetFraction) * state.pageSizeWithSpacing).roundToInt() } @@ -124,7 +128,6 @@ internal class PagerScrollPosition( delta / state.pageSizeWithSpacing.toFloat() } currentPageOffsetFraction += fractionDelta - state.remeasureTrigger = Unit // trigger remeasure } } 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 2ec4046839c..c38d4a989e3 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 @@ -31,6 +31,7 @@ import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState +import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf @@ -46,11 +47,12 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.AlignmentLine +import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.Remeasurement import androidx.compose.ui.layout.RemeasurementModifier import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import kotlin.math.abs import kotlin.math.absoluteValue @@ -156,8 +158,6 @@ abstract class PagerState( private var isScrollingForward: Boolean by mutableStateOf(false) - internal var remeasureTrigger by mutableStateOf(Unit, neverEqualPolicy()) - internal val scrollPosition = PagerScrollPosition(currentPage, currentPageOffsetFraction, this) internal var firstVisiblePage = currentPage @@ -199,14 +199,28 @@ abstract class PagerState( val newValue = absolute.coerceIn(0.0f, maxScrollOffset.toFloat()) val changed = absolute != newValue val consumed = newValue - currentScrollPosition - + previousPassDelta = consumed if (consumed.absoluteValue != 0.0f) { isScrollingForward = consumed > 0.0f } val consumedInt = consumed.roundToInt() - scrollPosition.applyScrollDelta(consumedInt) - previousPassDelta = consumed + + val layoutInfo = pagerLayoutInfoState.value + + if (layoutInfo.tryToApplyScrollWithoutRemeasure(-consumedInt)) { + debugLog { "Will Apply Without Remeasure" } + applyMeasureResult( + result = layoutInfo, + visibleItemsStayedTheSame = true + ) + // we don't need to remeasure, so we only trigger re-placement: + placementScopeInvalidator.invalidateScope() + } else { + debugLog { "Will Apply With Remeasure" } + scrollPosition.applyScrollDelta(consumedInt) + remeasurement?.forceRemeasure() + } accumulator = consumed - consumedInt // Avoid floating-point rounding error @@ -241,7 +255,8 @@ abstract class PagerState( private var wasPrefetchingForward = false /** Backing state for PagerLayoutInfo */ - private var pagerLayoutInfoState = mutableStateOf<PagerLayoutInfo>(EmptyLayoutInfo) + private var pagerLayoutInfoState = + mutableStateOf(EmptyLayoutInfo, neverEqualPolicy()) /** * A [PagerLayoutInfo] that contains useful information about the Pager's last layout pass. @@ -406,6 +421,8 @@ abstract class PagerState( internal val nearestRange: IntRange by scrollPosition.nearestRangeState + internal val placementScopeInvalidator = ObservableScopeInvalidator() + /** * Scroll (jump immediately) to a given [page]. * @@ -574,20 +591,27 @@ abstract class PagerState( /** * Updates the state with the new calculated scroll position and consumed scroll. */ - internal fun applyMeasureResult(result: PagerMeasureResult) { + internal fun applyMeasureResult( + result: PagerMeasureResult, + visibleItemsStayedTheSame: Boolean = false + ) { debugLog { "Applying Measure Result" } - scrollPosition.updateFromMeasureResult(result) + if (visibleItemsStayedTheSame) { + scrollPosition.updateCurrentPageOffsetFraction(result.currentPageOffsetFraction) + } else { + scrollPosition.updateFromMeasureResult(result) + cancelPrefetchIfVisibleItemsChanged(result) + } pagerLayoutInfoState.value = result canScrollForward = result.canScrollForward - canScrollBackward = (result.firstVisiblePage?.index ?: 0) != 0 || - result.firstVisiblePageScrollOffset != 0 + canScrollBackward = result.canScrollBackward numMeasurePasses++ result.firstVisiblePage?.let { firstVisiblePage = it.index } firstVisiblePageOffset = result.firstVisiblePageScrollOffset - cancelPrefetchIfVisibleItemsChanged(result) tryRunPrefetch(result) maxScrollOffset = result.calculateNewMaxScrollOffset(pageCount) - debugLog { "Finished Applying Measure Result" } + debugLog { "Finished Applying Measure Result" + + "\nNew maxScrollOffset=$maxScrollOffset" } } private fun tryRunPrefetch(result: PagerMeasureResult) = Snapshot.withoutReadObservation { @@ -707,19 +731,32 @@ private const val MaxPagesForAnimateScroll = 3 internal const val PagesToPrefetch = 1 @OptIn(ExperimentalFoundationApi::class) -internal object EmptyLayoutInfo : PagerLayoutInfo { - override val visiblePagesInfo: List<PageInfo> = emptyList() - override val pageSize: Int = 0 - override val pageSpacing: Int = 0 - override val beforeContentPadding: Int = 0 - override val afterContentPadding: Int = 0 - override val viewportSize: IntSize = IntSize.Zero - override val orientation: Orientation = Orientation.Horizontal - override val viewportStartOffset: Int = 0 - override val viewportEndOffset: Int = 0 - override val reverseLayout: Boolean = false - override val beyondBoundsPageCount: Int = 0 -} +internal val EmptyLayoutInfo = PagerMeasureResult( + visiblePagesInfo = emptyList(), + pageSize = 0, + pageSpacing = 0, + afterContentPadding = 0, + orientation = Orientation.Horizontal, + viewportStartOffset = 0, + viewportEndOffset = 0, + reverseLayout = false, + beyondBoundsPageCount = 0, + firstVisiblePage = null, + firstVisiblePageScrollOffset = 0, + currentPage = null, + currentPageOffsetFraction = 0.0f, + canScrollForward = false, + measureResult = object : MeasureResult { + override val width: Int = 0 + + override val height: Int = 0 + @Suppress("PrimitiveInCollection") + override val alignmentLines: Map<AlignmentLine, Int> = mapOf() + + override fun placeChildren() {} + }, + remeasureNeeded = false +) private val UnitDensity = object : Density { override val density: Float = 1f |