aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-06 22:03:08 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2023-12-06 22:03:08 +0000
commit10035bb95102a1761733ddc6ded07d3963bc307b (patch)
treee382ee60aae3af5bf282bf81deae8de49240ea3d
parent224977ad5d1e9eb74a78a3fbe81ec2f27bf2ff6c (diff)
parent25adc5acf10067b8a8ab04b28c6476e0b4e275e4 (diff)
downloadsupport-10035bb95102a1761733ddc6ded07d3963bc307b.tar.gz
Merge "Only do relayout when pager scrolling doesn't require composing/disposing" into snap-temp-L11100030000693181
-rw-r--r--compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt5
-rw-r--r--compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt4
-rw-r--r--compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt101
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt16
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt33
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt2
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt82
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt5
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt89
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