Merge changes I8acb28c2,I0008a425 into androidx-main
* changes:
Do less recompositions when the sliding window updates
Ensure Lazy layouts recomposition with the same input doesn't cause remeasure
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
index 5848b5c..ba84f74 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
@@ -44,6 +44,8 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
@@ -1031,5 +1033,75 @@
rule.onNodeWithTag("7").assertDoesNotExist()
}
+ @Test
+ fun recomposingWithNewComposedModifierObjectIsNotCausingRemeasure() {
+ var remeasureCount = 0
+ val layoutModifier = Modifier.layout { measurable, constraints ->
+ remeasureCount++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ val counter = mutableStateOf(0)
+
+ rule.setContentWithTestViewConfiguration {
+ counter.value // just to trigger recomposition
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ // this will return a new object everytime causing LazyVerticalGrid recomposition
+ // without causing remeasure
+ Modifier.composed { layoutModifier }
+ ) {
+ items(1) {
+ Spacer(Modifier.size(10.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(remeasureCount).isEqualTo(1)
+ counter.value++
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(remeasureCount).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun scrollingALotDoesntCauseLazyLayoutRecomposition() {
+ var recomposeCount = 0
+ lateinit var state: LazyGridState
+
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyGridState()
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ Modifier.composed {
+ recomposeCount++
+ Modifier
+ }.size(100.dp),
+ state
+ ) {
+ items(1000) {
+ Spacer(Modifier.size(100.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(recomposeCount).isEqualTo(1)
+
+ runBlocking {
+ state.scrollToItem(100)
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(recomposeCount).isEqualTo(1)
+ }
+ }
+
// TODO: add tests for the cache logic
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index c546d2f25..6192186 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -44,10 +44,12 @@
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsProperties
@@ -1646,6 +1648,74 @@
rule.onNodeWithTag("3").assertIsDisplayed()
}
+ @Test
+ fun recomposingWithNewComposedModifierObjectIsNotCausingRemeasure() {
+ var remeasureCount = 0
+ val layoutModifier = Modifier.layout { measurable, constraints ->
+ remeasureCount++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ val counter = mutableStateOf(0)
+
+ rule.setContentWithTestViewConfiguration {
+ counter.value // just to trigger recomposition
+ LazyColumnOrRow(
+ // this will return a new object everytime causing Lazy list recomposition
+ // without causing remeasure
+ Modifier.composed { layoutModifier }
+ ) {
+ items(1) {
+ Spacer(Modifier.size(10.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasureCount).isEqualTo(1)
+ counter.value++
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasureCount).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun scrollingALotDoesntCauseLazyLayoutRecomposition() {
+ var recomposeCount = 0
+ lateinit var state: LazyListState
+
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyListState()
+ LazyColumnOrRow(
+ Modifier.composed {
+ recomposeCount++
+ Modifier
+ },
+ state
+ ) {
+ items(1000) {
+ Spacer(Modifier.size(10.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(recomposeCount).isEqualTo(1)
+
+ runBlocking {
+ state.scrollToItem(100)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(recomposeCount).isEqualTo(1)
+ }
+ }
+
// ********************* END OF TESTS *********************
// Helper functions, etc. live below here
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 08ed113..e80d43d 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
@@ -109,10 +109,7 @@
state.prefetchPolicy = rememberLazyLayoutPrefetchPolicy()
val innerState = rememberLazyLayoutState().also { state.innerState = it }
- val itemsProvider = stateOfItemsProvider.value
- if (itemsProvider.itemsCount > 0) {
- state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
- }
+ ScrollPositionUpdater(stateOfItemsProvider, state)
LazyLayout(
modifier = modifier
@@ -152,6 +149,19 @@
)
}
+/** Extracted to minimize the recomposition scope */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun ScrollPositionUpdater(
+ stateOfItemsProvider: State<LazyGridItemsProvider>,
+ state: LazyGridState
+) {
+ val itemsProvider = stateOfItemsProvider.value
+ if (itemsProvider.itemsCount > 0) {
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
+ }
+}
+
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun rememberLazyGridMeasurePolicy(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProviderImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
index 7ea9ee4..8953fbd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
@@ -45,7 +45,7 @@
val latestContent = rememberUpdatedState(content)
val nearestItemsRangeState = remember(state) {
mutableStateOf(
- calculateNearestItemsRange(state.firstVisibleItemIndex)
+ calculateNearestItemsRange(state.firstVisibleItemIndexNonObservable.value)
)
}
LaunchedEffect(nearestItemsRangeState) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
index 47844fd..4622ceb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
@@ -20,7 +20,9 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.lazy.LazyGridState
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.ScrollAxisRange
@@ -35,6 +37,8 @@
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
+@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
+@Composable
internal fun Modifier.lazyGridSemantics(
stateOfItemsProvider: State<LazyGridItemsProvider>,
state: LazyGridState,
@@ -42,16 +46,24 @@
isVertical: Boolean,
reverseScrolling: Boolean,
userScrollEnabled: Boolean
-): Modifier {
- return semantics {
- indexForKey { needle ->
+) = this.then(
+ remember(
+ stateOfItemsProvider,
+ state,
+ isVertical,
+ reverseScrolling,
+ userScrollEnabled
+ ) {
+ val indexForKeyMapping: (Any) -> Int = { needle ->
val key = stateOfItemsProvider.value::getKey
+ var result = -1
for (index in 0 until stateOfItemsProvider.value.itemsCount) {
if (key(index) == needle) {
- return@indexForKey index
+ result = index
+ break
}
}
- -1
+ result
}
val accessibilityScrollState = ScrollAxisRange(
@@ -74,23 +86,26 @@
},
reverseScrolling = reverseScrolling
)
- if (isVertical) {
- verticalScrollAxisRange = accessibilityScrollState
- } else {
- horizontalScrollAxisRange = accessibilityScrollState
- }
- if (userScrollEnabled) {
- scrollBy { x, y ->
- val delta = if (isVertical) { y } else { x }
+ val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
+ { x, y ->
+ val delta = if (isVertical) {
+ y
+ } else {
+ x
+ }
coroutineScope.launch {
(state as ScrollableState).animateScrollBy(delta)
}
// TODO(aelias): is it important to return false if we know in advance we cannot scroll?
true
}
+ } else {
+ null
+ }
- scrollToIndex { index ->
+ val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
+ { index ->
require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
"Can't scroll to index $index, it is out of " +
"bounds [0, ${state.layoutInfo.totalItemsCount})"
@@ -100,9 +115,31 @@
}
true
}
+ } else {
+ null
}
// TODO(popam): check if this is correct - it would be nice to provide correct columns here
- collectionInfo = CollectionInfo(rowCount = -1, columnCount = -1)
+ val collectionInfo = CollectionInfo(rowCount = -1, columnCount = -1)
+
+ Modifier.semantics {
+ indexForKey(indexForKeyMapping)
+
+ if (isVertical) {
+ verticalScrollAxisRange = accessibilityScrollState
+ } else {
+ horizontalScrollAxisRange = accessibilityScrollState
+ }
+
+ if (scrollByAction != null) {
+ scrollBy(action = scrollByAction)
+ }
+
+ if (scrollToIndexAction != null) {
+ scrollToIndex(action = scrollToIndexAction)
+ }
+
+ this.collectionInfo = collectionInfo
+ }
}
-}
+)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt
index 715dfaf..291d2b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt
@@ -107,10 +107,7 @@
state.prefetchPolicy = rememberLazyLayoutPrefetchPolicy()
val innerState = rememberLazyLayoutState().also { state.innerState = it }
- val itemsProvider = stateOfItemsProvider.value
- if (itemsProvider.itemsCount > 0) {
- state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
- }
+ ScrollPositionUpdater(stateOfItemsProvider, state)
LazyLayout(
modifier = modifier
@@ -149,6 +146,18 @@
)
}
+/** Extracted to minimize the recomposition scope */
+@Composable
+private fun ScrollPositionUpdater(
+ stateOfItemsProvider: State<LazyListItemsProvider>,
+ state: LazyListState
+) {
+ val itemsProvider = stateOfItemsProvider.value
+ if (itemsProvider.itemsCount > 0) {
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
+ }
+}
+
@Composable
private fun rememberLazyListMeasurePolicy(
/** State containing the items provider of the list. */
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt
index 2a3753c..797138d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt
@@ -19,7 +19,9 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.ScrollAxisRange
@@ -33,6 +35,8 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
+@Composable
internal fun Modifier.lazyListSemantics(
stateOfItemsProvider: State<LazyListItemsProvider>,
state: LazyListState,
@@ -40,16 +44,24 @@
isVertical: Boolean,
reverseScrolling: Boolean,
userScrollEnabled: Boolean
-): Modifier {
- return semantics {
- indexForKey { needle ->
+) = this.then(
+ remember(
+ stateOfItemsProvider,
+ state,
+ isVertical,
+ reverseScrolling,
+ userScrollEnabled
+ ) {
+ val indexForKeyMapping: (Any) -> Int = { needle ->
val key = stateOfItemsProvider.value::getKey
+ var result = -1
for (index in 0 until stateOfItemsProvider.value.itemsCount) {
if (key(index) == needle) {
- return@indexForKey index
+ result = index
+ break
}
}
- -1
+ result
}
val accessibilityScrollState = ScrollAxisRange(
@@ -72,14 +84,8 @@
},
reverseScrolling = reverseScrolling
)
- if (isVertical) {
- verticalScrollAxisRange = accessibilityScrollState
- } else {
- horizontalScrollAxisRange = accessibilityScrollState
- }
-
- if (userScrollEnabled) {
- scrollBy { x, y ->
+ val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
+ { x, y ->
val delta = if (isVertical) {
y
} else {
@@ -91,8 +97,12 @@
// TODO(aelias): is it important to return false if we know in advance we cannot scroll?
true
}
+ } else {
+ null
+ }
- scrollToIndex { index ->
+ val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
+ { index ->
require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
"Can't scroll to index $index, it is out of " +
"bounds [0, ${state.layoutInfo.totalItemsCount})"
@@ -102,11 +112,33 @@
}
true
}
+ } else {
+ null
}
- collectionInfo = CollectionInfo(
+ val collectionInfo = CollectionInfo(
rowCount = if (isVertical) -1 else 1,
columnCount = if (isVertical) 1 else -1
)
+
+ Modifier.semantics {
+ indexForKey(indexForKeyMapping)
+
+ if (isVertical) {
+ verticalScrollAxisRange = accessibilityScrollState
+ } else {
+ horizontalScrollAxisRange = accessibilityScrollState
+ }
+
+ if (scrollByAction != null) {
+ scrollBy(action = scrollByAction)
+ }
+
+ if (scrollToIndexAction != null) {
+ scrollToIndex(action = scrollToIndexAction)
+ }
+
+ this.collectionInfo = collectionInfo
+ }
}
-}
+)