Merge "Add support for lazy with commit" into androidx-main
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
index 5d4cdac..8500c36 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
@@ -18,11 +18,22 @@
import android.os.Build
import androidx.annotation.RequiresApi
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -48,6 +59,8 @@
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import kotlin.test.Ignore
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -742,6 +755,103 @@
rule.runOnIdle { verifyNoMoreInteractions(am) }
}
+ @Test
+ @SmallTest
+ fun autofillManager_lazyColumnScroll_callsCommit() {
+ lateinit var state: LazyListState
+ lateinit var coroutineScope: CoroutineScope
+ val am: PlatformAutofillManager = mock()
+ val count = 100
+
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
+ state = rememberLazyListState()
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+
+ LazyColumn(Modifier.fillMaxWidth().height(50.dp), state) {
+ item {
+ Box(
+ Modifier.semantics {
+ contentType = ContentType.Username
+ contentDataType = ContentDataType.Text
+ }
+ .size(10.dp)
+ )
+ }
+ items(count) { Box(Modifier.size(10.dp)) }
+ }
+ }
+
+ rule.runOnIdle { coroutineScope.launch { state.scrollToItem(10) } }
+
+ rule.runOnIdle { verify(am).commit() }
+ }
+
+ @Test
+ @SmallTest
+ fun autofillManager_columnScroll_doesNotCallCommit() {
+ lateinit var scrollState: ScrollState
+ lateinit var coroutineScope: CoroutineScope
+ val am: PlatformAutofillManager = mock()
+
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
+ scrollState = rememberScrollState()
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+
+ Column(Modifier.fillMaxWidth().height(50.dp).verticalScroll(scrollState)) {
+ Row {
+ Box(
+ Modifier.semantics {
+ contentType = ContentType.Username
+ contentDataType = ContentDataType.Text
+ }
+ .size(10.dp)
+ )
+ }
+ repeat(50) { Box(Modifier.size(10.dp)) }
+ }
+ }
+
+ rule.runOnIdle { coroutineScope.launch { scrollState.scrollTo(scrollState.maxValue / 2) } }
+
+ // Scrolling past an element in a column is not enough to call commit
+ rule.runOnIdle { verify(am, never()).commit() }
+ }
+
+ @Test
+ @SmallTest
+ fun autofillManager_column_nodesDisappearingCallsCommit() {
+ lateinit var scrollState: ScrollState
+ val am: PlatformAutofillManager = mock()
+ var autofillComponentsVisible by mutableStateOf(true)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ scrollState = rememberScrollState()
+
+ Column(Modifier.fillMaxWidth().height(50.dp).verticalScroll(scrollState)) {
+ if (autofillComponentsVisible) {
+ Row {
+ Box(
+ Modifier.semantics {
+ contentType = ContentType.Username
+ contentDataType = ContentDataType.Text
+ }
+ .size(10.dp)
+ )
+ }
+ }
+ repeat(50) { Box(Modifier.size(10.dp)) }
+ }
+ }
+
+ rule.runOnIdle { autofillComponentsVisible = false }
+
+ // A column disappearing will call commit
+ rule.runOnIdle { verify(am).commit() }
+ }
+
// ============================================================================================
// Helper functions
// ============================================================================================
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 3a2f75d..d3fed6e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -227,9 +227,27 @@
}
}
- internal fun onDetach(semanticsInfo: SemanticsInfo) {
+ internal fun onPostLayoutNodeReused(semanticsInfo: SemanticsInfo, previousSemanticsId: Int) {
+ if (currentlyDisplayedIDs.remove(previousSemanticsId)) {
+ pendingChangesToDisplayedIds = true
+ }
if (semanticsInfo.semanticsConfiguration?.isRelatedToAutofill() == true) {
- currentlyDisplayedIDs.remove(semanticsInfo.semanticsId)
+ currentlyDisplayedIDs.add(semanticsInfo.semanticsId)
+ pendingChangesToDisplayedIds = true
+ }
+ }
+
+ internal fun onLayoutNodeDeactivated(semanticsInfo: SemanticsInfo) {
+ if (currentlyDisplayedIDs.remove(semanticsInfo.semanticsId)) {
+ pendingChangesToDisplayedIds = true
+ // TODO(MNUZEN): Notify autofill manager that a node has been removed.
+ // platformAutofillManager
+ // .notifyViewVisibilityChanged(view, semanticsInfo.semanticsId, false)
+ }
+ }
+
+ internal fun onDetach(semanticsInfo: SemanticsInfo) {
+ if (currentlyDisplayedIDs.remove(semanticsInfo.semanticsId)) {
pendingChangesToDisplayedIds = true
// TODO(MNUZEN): Notify autofill manager that a node has been removed.
// platformAutofillManager
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 8872548..5197336 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -1802,14 +1802,25 @@
if (ComposeUiFlags.isRectTrackingEnabled) {
rectManager.remove(layoutNode)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+ _autofillManager?.onLayoutNodeDeactivated(layoutNode)
+ }
}
- override fun onLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {
+ override fun onPreLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {
// Keep the mapping up to date when the semanticsId changes
layoutNodes.remove(oldSemanticsId)
layoutNodes[layoutNode.semanticsId] = layoutNode
}
+ override fun onPostLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+ _autofillManager?.onPostLayoutNodeReused(layoutNode, oldSemanticsId)
+ }
+ }
+
override fun onInteropViewLayoutChange(view: InteropView) {
isPendingInteropViewLayoutChangeDispatch = true
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index bc99aece..5f60779 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -1377,7 +1377,7 @@
}
val oldSemanticsId = semanticsId
semanticsId = generateSemanticsId()
- owner?.onLayoutNodeReused(this, oldSemanticsId)
+ owner?.onPreLayoutNodeReused(this, oldSemanticsId)
// resetModifierState detaches all nodes, so we need to re-attach them upon reuse.
nodes.markAsAttached()
nodes.runAttachLifecycle()
@@ -1386,6 +1386,7 @@
invalidateSemantics()
}
rescheduleRemeasureOrRelayout(this)
+ owner?.onPostLayoutNodeReused(this, oldSemanticsId)
}
override fun onDeactivate() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 2f4b773..a75bf87 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -289,8 +289,18 @@
fun onLayoutNodeDeactivated(layoutNode: LayoutNode)
- /** Called to do internal upkeep when a [layoutNode] is reused. */
- fun onLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {}
+ /**
+ * Called to do internal upkeep when a [layoutNode] is reused. The modifier nodes of this layout
+ * node have not been attached by the time this method finishes running.
+ */
+ fun onPreLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {}
+
+ /**
+ * Called to do internal upkeep when a [layoutNode] is reused. This is only called after
+ * [onPreLayoutNodeReused], at which point the modifier nodes of this layout node have been
+ * attached.
+ */
+ fun onPostLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {}
/**
* The position and/or size of an interop view (typically, an android.view.View) has changed. On