Merge "Only do relayout when pager scrolling doesn't require composing/disposing" into androidx-main
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 614d0d4..1350683 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
@@ -58,6 +58,7 @@
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
@@ -357,12 +358,12 @@
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 69ef744..71635fb 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 @@
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 b94c526..37a5df7 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 @@
import androidx.compose.foundation.AutoTestFrameClock
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.gestures.snapping.SnapPosition
import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
import androidx.compose.foundation.layout.Box
@@ -41,7 +42,9 @@
import androidx.compose.ui.util.fastMaxBy
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import kotlin.math.abs
+import kotlin.math.roundToInt
import kotlin.test.assertFalse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -537,6 +540,105 @@
}
}
+ @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 45748d4..6586433 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 @@
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 @@
}
}
+ 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 c60b0b8..bef2c4f 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.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 @@
beyondBoundsPageCount: Int,
pinnedPages: List<Int>,
snapPosition: SnapPosition,
+ placementScopeInvalidator: ObservableScopeInvalidator,
layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): PagerMeasureResult {
require(beforeContentPadding >= 0) { "negative beforeContentPadding" }
@@ -86,7 +88,8 @@
canScrollForward = false,
currentPage = null,
currentPageOffsetFraction = 0.0f,
- snapPosition = snapPosition
+ snapPosition = snapPosition,
+ remeasureNeeded = false
)
} else {
@@ -185,10 +188,24 @@
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++
+ }
}
debugLog { "Composing Forward Starting at Index=$index" }
@@ -223,9 +240,10 @@
}
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)
@@ -424,6 +442,8 @@
positionedPages.fastForEach {
it.place(this)
}
+ // we attach it during the placement so PagerState can trigger re-placement
+ placementScopeInvalidator.attachToScope()
},
viewportStartOffset = -beforeContentPadding,
viewportEndOffset = maxOffset + afterContentPadding,
@@ -437,7 +457,8 @@
canScrollForward = index < pageCount || currentMainAxisOffset > maxOffset,
currentPage = newCurrentPage,
currentPageOffsetFraction = currentPageOffsetFraction,
- snapPosition = snapPosition
+ snapPosition = snapPosition,
+ 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 f255c9e..f5e80fa 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
@@ -181,8 +181,8 @@
reverseLayout = reverseLayout,
pinnedPages = pinnedPages,
snapPosition = snapPosition,
+ 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 c236604..4d193eb 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
@@ -21,10 +21,11 @@
import androidx.compose.foundation.gestures.snapping.SnapPosition
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,
@@ -35,13 +36,86 @@
override val beyondBoundsPageCount: Int,
val firstVisiblePage: MeasuredPage?,
val currentPage: MeasuredPage?,
- val currentPageOffsetFraction: Float,
- val firstVisiblePageScrollOffset: Int,
- val canScrollForward: Boolean,
+ var currentPageOffsetFraction: Float,
+ var firstVisiblePageScrollOffset: Int,
+ var canScrollForward: Boolean,
override val snapPosition: SnapPosition,
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 ea35c64..c7a96e6 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
@@ -106,6 +106,10 @@
currentPageOffsetFraction = offsetFraction
}
+ fun updateCurrentPageOffsetFraction(offsetFraction: Float) {
+ currentPageOffsetFraction = offsetFraction
+ }
+
fun currentAbsoluteScrollOffset(): Int {
return ((currentPage +
currentPageOffsetFraction) * state.pageSizeWithSpacing).roundToInt()
@@ -119,7 +123,6 @@
delta / state.pageSizeWithSpacing.toFloat()
}
currentPageOffsetFraction += fractionUpdate
- 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 3f808a9..0459e93 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.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.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
@@ -175,8 +177,6 @@
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
@@ -218,14 +218,28 @@
val newValue = absolute.coerceIn(0.0f, maxScrollOffset)
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
@@ -260,7 +274,8 @@
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.
@@ -425,6 +440,8 @@
internal val nearestRange: IntRange by scrollPosition.nearestRangeState
+ internal val placementScopeInvalidator = ObservableScopeInvalidator()
+
/**
* Scroll (jump immediately) to a given [page].
*
@@ -592,20 +609,27 @@
/**
* 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 {
@@ -725,20 +749,33 @@
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
- override val snapPosition: SnapPosition = SnapPosition.Start
-}
+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,
+ snapPosition = SnapPosition.Start,
+ 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