Merge "Handle children UseCase state changes." into androidx-main
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
index 8ca7900..bc68ffa 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
@@ -87,6 +87,36 @@
"key": "notAnnotation",
"value": "androidx.test.filters.FlakyTest"
}
+ ],
+ "additionalApkKeys": []
+ }
+ """.trimIndent()
+ )
+ )
+ }
+
+ @Test
+ fun testJsonAgainstGoldenAdditionalApkKey() {
+ builder.additionalApkKeys(listOf("customKey"))
+ MatcherAssert.assertThat(
+ builder.buildJson(),
+ CoreMatchers.`is`("""
+ {
+ "name": "placeHolderAndroidTest.xml",
+ "minSdkVersion": "15",
+ "testSuiteTags": [
+ "placeholder_tag"
+ ],
+ "testApk": "placeholder.apk",
+ "testApkSha256": "123456",
+ "instrumentationArgs": [
+ {
+ "key": "notAnnotation",
+ "value": "androidx.test.filters.FlakyTest"
+ }
+ ],
+ "additionalApkKeys": [
+ "customKey"
]
}
""".trimIndent()
@@ -118,7 +148,8 @@
"key": "androidx.benchmark.dryRunMode.enable",
"value": "true"
}
- ]
+ ],
+ "additionalApkKeys": []
}
""".trimIndent()
)
@@ -147,7 +178,8 @@
"key": "notAnnotation",
"value": "androidx.test.filters.FlakyTest"
}
- ]
+ ],
+ "additionalApkKeys": []
}
""".trimIndent()
)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index d6ec9a5..b5e2ada 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -342,6 +342,8 @@
var disableDeviceTests = false
+ val additionalDeviceTestApkKeys = mutableListOf<String>()
+
fun shouldEnforceKotlinStrictApiMode(): Boolean {
return !legacyDisableKotlinStrictApiMode &&
shouldConfigureApiTasks()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
index 1907576..52cc075 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
@@ -28,10 +28,11 @@
lateinit var minSdk: String
var runAllTests: Boolean = true
var cleanupApks: Boolean = true
- val tags: MutableList<String> = mutableListOf()
+ val tags = mutableListOf<String>()
lateinit var testApkName: String
lateinit var testApkSha256: String
lateinit var testRunner: String
+ val additionalApkKeys = mutableListOf<String>()
fun configName(configName: String) = apply { this.configName = configName }
fun appApkName(appApkName: String) = apply { this.appApkName = appApkName }
@@ -43,6 +44,7 @@
fun runAllTests(runAllTests: Boolean) = apply { this.runAllTests = runAllTests }
fun cleanupApks(cleanupApks: Boolean) = apply { this.cleanupApks = cleanupApks }
fun tag(tag: String) = apply { this.tags.add(tag) }
+ fun additionalApkKeys(keys: List<String>) = apply { additionalApkKeys.addAll(keys) }
fun testApkName(testApkName: String) = apply { this.testApkName = testApkName }
fun testApkSha256(testApkSha256: String) = apply { this.testApkSha256 = testApkSha256 }
fun testRunner(testRunner: String) = apply { this.testRunner = testRunner }
@@ -72,7 +74,8 @@
"testApkSha256" to testApkSha256,
"appApk" to appApkName,
"appApkSha256" to appApkSha256,
- "instrumentationArgs" to instrumentationArgs
+ "instrumentationArgs" to instrumentationArgs,
+ "additionalApkKeys" to additionalApkKeys
)
if (isBenchmark && !isPostsubmit) {
values["instrumentationArgs"]
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index ad5f8e4..ee7c132 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -33,6 +33,7 @@
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import java.io.File
+import org.gradle.api.provider.ListProperty
/**
* Writes a configuration file in
@@ -84,6 +85,9 @@
@get:Input
abstract val presubmit: Property<Boolean>
+ @get:Input
+ abstract val additionalApkKeys: ListProperty<String>
+
@get:OutputFile
abstract val outputXml: RegularFileProperty
@@ -150,6 +154,7 @@
.appApkSha256(sha256(File(appApkBuiltArtifact.outputFile)))
testApkSha256Report.addFile(appName, appApkBuiltArtifact)
}
+ configBuilder.additionalApkKeys(additionalApkKeys.get())
val isPresubmit = presubmit.get()
configBuilder.isPostsubmit(!isPresubmit)
// Will be using the constrained configs for all devices api 26 and below.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 7a4f404..b890f55 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -81,6 +81,7 @@
task.testFolder.set(artifacts.get(SingleArtifact.APK))
task.testLoader.set(artifacts.getBuiltArtifactsLoader())
+ task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
task.outputXml.fileValue(File(getTestConfigDirectory(), xmlName))
task.outputJson.fileValue(File(getTestConfigDirectory(), jsonName))
task.shaReportOutput.fileValue(File(getTestConfigDirectory(), sha256XmlName))
@@ -337,9 +338,11 @@
val configTask = getOrCreateMacrobenchmarkConfigTask(variantName)
if (path.endsWith("macrobenchmark")) {
configTask.configure { task ->
+ val androidXExtension = extensions.getByType<AndroidXExtension>()
val fileNamePrefix = "${this.path.asFilenamePrefix()}$variantName"
task.testFolder.set(artifacts.get(SingleArtifact.APK))
task.testLoader.set(artifacts.getBuiltArtifactsLoader())
+ task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
task.outputXml.fileValue(
File(getTestConfigDirectory(), "$fileNamePrefix.xml")
)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index a38510d..3f4139a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -25,6 +25,8 @@
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
import static androidx.core.util.Preconditions.checkArgument;
+import static java.util.UUID.randomUUID;
+
import android.graphics.Rect;
import android.util.Size;
@@ -54,6 +56,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
/**
* A {@link Node} implementation that wraps around the public {@link SurfaceProcessor} interface.
@@ -328,6 +331,15 @@
public abstract static class OutConfig {
/**
+ * Unique ID of the config.
+ *
+ * <p> This is for making sure two {@link OutConfig} with the same value can be stored as
+ * different keys in a {@link HashMap}.
+ */
+ @NonNull
+ abstract UUID getUuid();
+
+ /**
* The target {@link UseCase} of the output stream.
*/
@CameraEffect.Targets
@@ -371,7 +383,8 @@
@NonNull
public static OutConfig of(int targets, @NonNull Rect cropRect, @NonNull Size size,
boolean mirroring) {
- return new AutoValue_SurfaceProcessorNode_OutConfig(targets, cropRect, size, mirroring);
+ return new AutoValue_SurfaceProcessorNode_OutConfig(randomUUID(), targets, cropRect,
+ size, mirroring);
}
}
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 5628d5f..c949451 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -16,6 +16,7 @@
package androidx.camera.core.processing
+import android.graphics.Matrix
import android.graphics.Rect
import android.graphics.SurfaceTexture
import android.os.Build
@@ -109,6 +110,30 @@
}
@Test
+ fun identicalOutConfigs_returnDifferentEdges() {
+ // Arrange: create 2 OutConfig with identical values
+ createSurfaceProcessorNode()
+ val inputEdge = SurfaceEdge(
+ PREVIEW,
+ StreamSpec.builder(INPUT_SIZE).build(),
+ Matrix.IDENTITY_MATRIX,
+ true,
+ PREVIEW_CROP_RECT,
+ 0,
+ false
+ )
+ val outConfig1 = OutConfig.of(inputEdge)
+ val outConfig2 = OutConfig.of(inputEdge)
+ val input = SurfaceProcessorNode.In.of(inputEdge, listOf(outConfig1, outConfig2))
+ // Act.
+ val output = node.transform(input)
+ // Assert: there are two outputs
+ assertThat(output).hasSize(2)
+ // Cleanup
+ inputEdge.close()
+ }
+
+ @Test
fun transformInput_receivesSurfaceRequest() {
// Arrange.
createSurfaceProcessorNode()
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 88f7362..3c6a103 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -36,7 +36,7 @@
*/
api("androidx.annotation:annotation:1.1.0")
api("androidx.compose.animation:animation:1.2.1")
- api("androidx.compose.runtime:runtime:1.3.1")
+ api(project(":compose:runtime:runtime"))
api(project(":compose:ui:ui"))
implementation(libs.kotlinStdlibCommon)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index 6db15c3..dbaeb55 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -29,15 +29,19 @@
import androidx.compose.foundation.relocation.bringIntoViewResponder
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ReusableContent
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
@@ -426,7 +430,11 @@
state = state
) {
items(items.size) {
- Box(Modifier.requiredSize(10.dp).testTag("$it").focusable())
+ Box(
+ Modifier
+ .requiredSize(10.dp)
+ .testTag("$it")
+ .focusable())
}
}
}
@@ -489,4 +497,42 @@
assertThat(container2Pinned).isTrue()
}
}
+
+ @Test
+ fun reusingFocusedItem_itemIsNotFocusedAnymore() {
+ // Arrange.
+ val focusRequester = FocusRequester()
+ lateinit var state: FocusState
+ var key by mutableStateOf(0)
+ rule.setContent {
+ ReusableContent(key) {
+ BasicText(
+ "focusableText",
+ modifier = Modifier
+ .testTag(focusTag)
+ .focusRequester(focusRequester)
+ .onFocusEvent { state = it }
+ .focusable()
+ )
+ }
+ }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ assertThat(state.isFocused).isTrue()
+ }
+ rule.onNodeWithTag(focusTag)
+ .assertIsFocused()
+
+ // Act.
+ rule.runOnIdle {
+ key = 1
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(state.isFocused).isFalse()
+ }
+ rule.onNodeWithTag(focusTag)
+ .assertIsNotFocused()
+ }
}
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 5ec853d..3c04288 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -350,6 +350,17 @@
public final class ExposedDropdownMenuKt {
}
+ @kotlin.jvm.JvmInline public final value class FabPosition {
+ field public static final androidx.compose.material3.FabPosition.Companion Companion;
+ }
+
+ public static final class FabPosition.Companion {
+ method public int getCenter();
+ method public int getEnd();
+ property public final int Center;
+ property public final int End;
+ }
+
public final class FloatingActionButtonDefaults {
method public androidx.compose.material3.FloatingActionButtonElevation bottomAppBarFabElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
@@ -597,7 +608,14 @@
method @androidx.compose.runtime.Composable public static void RadioButton(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit>? onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.RadioButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
}
+ public final class ScaffoldDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getContentWindowInsets();
+ property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets contentWindowInsets;
+ field public static final androidx.compose.material3.ScaffoldDefaults INSTANCE;
+ }
+
public final class ScaffoldKt {
+ method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
}
public final class SearchBarDefaults {
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 8eec45e..a9fd87e 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -495,7 +495,7 @@
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ExposedDropdownMenuBox(boolean expanded, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onExpandedChange, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ExposedDropdownMenuBoxScope,kotlin.Unit> content);
}
- @androidx.compose.material3.ExperimentalMaterial3Api @kotlin.jvm.JvmInline public final value class FabPosition {
+ @kotlin.jvm.JvmInline public final value class FabPosition {
field public static final androidx.compose.material3.FabPosition.Companion Companion;
}
@@ -820,14 +820,14 @@
property public boolean isVisible;
}
- @androidx.compose.material3.ExperimentalMaterial3Api public final class ScaffoldDefaults {
+ public final class ScaffoldDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getContentWindowInsets();
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets contentWindowInsets;
field public static final androidx.compose.material3.ScaffoldDefaults INSTANCE;
}
public final class ScaffoldKt {
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
}
@androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 5ec853d..3c04288 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -350,6 +350,17 @@
public final class ExposedDropdownMenuKt {
}
+ @kotlin.jvm.JvmInline public final value class FabPosition {
+ field public static final androidx.compose.material3.FabPosition.Companion Companion;
+ }
+
+ public static final class FabPosition.Companion {
+ method public int getCenter();
+ method public int getEnd();
+ property public final int Center;
+ property public final int End;
+ }
+
public final class FloatingActionButtonDefaults {
method public androidx.compose.material3.FloatingActionButtonElevation bottomAppBarFabElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation elevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
@@ -597,7 +608,14 @@
method @androidx.compose.runtime.Composable public static void RadioButton(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit>? onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.RadioButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
}
+ public final class ScaffoldDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getContentWindowInsets();
+ property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets contentWindowInsets;
+ field public static final androidx.compose.material3.ScaffoldDefaults INSTANCE;
+ }
+
public final class ScaffoldKt {
+ method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
}
public final class SearchBarDefaults {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
index bf5bc93..605e1fb 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
@@ -58,7 +58,6 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalMaterial3Api::class)
class ScaffoldTest {
@get:Rule
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index 52d652c..122937e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -70,7 +70,6 @@
* properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
* the child of the scroll, and not on the scroll itself.
*/
-@ExperimentalMaterial3Api
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
@@ -109,7 +108,6 @@
* @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
* [content], typically a [NavigationBar].
*/
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ScaffoldLayout(
fabPosition: FabPosition,
@@ -273,7 +271,6 @@
/**
* Object containing various default values for [Scaffold] component.
*/
-@ExperimentalMaterial3Api
object ScaffoldDefaults {
/**
* Default insets to be used and consumed by the scaffold content slot
@@ -286,7 +283,6 @@
/**
* The possible positions for a [FloatingActionButton] attached to a [Scaffold].
*/
-@ExperimentalMaterial3Api
@kotlin.jvm.JvmInline
value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
companion object {
diff --git a/compose/ui/ui/api/public_plus_experimental_1.4.0-beta01.txt b/compose/ui/ui/api/public_plus_experimental_1.4.0-beta01.txt
index d167cda..01283ce 100644
--- a/compose/ui/ui/api/public_plus_experimental_1.4.0-beta01.txt
+++ b/compose/ui/ui/api/public_plus_experimental_1.4.0-beta01.txt
@@ -158,6 +158,7 @@
method public final boolean isAttached();
method public void onAttach();
method public void onDetach();
+ method public void onReset();
method public final void sideEffect(kotlin.jvm.functions.Function0<kotlin.Unit> effect);
property public final boolean isAttached;
property public final androidx.compose.ui.Modifier.Node node;
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index d167cda..01283ce 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -158,6 +158,7 @@
method public final boolean isAttached();
method public void onAttach();
method public void onDetach();
+ method public void onReset();
method public final void sideEffect(kotlin.jvm.functions.Function0<kotlin.Unit> effect);
property public final boolean isAttached;
property public final androidx.compose.ui.Modifier.Node node;
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
index 56d110a..01ad27a 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
@@ -26,6 +26,9 @@
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -313,3 +316,20 @@
}
)
}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Sampled
+@Composable
+fun ModifierNodeResetSample() {
+ class SelectableNode : Modifier.Node() {
+ var selected by mutableStateOf(false)
+
+ override fun onReset() {
+ // reset `selected` to the initial value as if the node will be reused for
+ // displaying different content it shouldn't be selected straight away.
+ selected = false
+ }
+
+ // some logic which sets `selected` to true when it is selected
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index 1e844e4..4358f39 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -2978,7 +2978,7 @@
val child = rule.onNodeWithTag("child").fetchSemanticsNode()
rule.runOnIdle {
- child.layoutNode.innerCoordinator.detach()
+ child.layoutNode.innerCoordinator.onRelease()
}
rule.runOnIdle {
@@ -3006,8 +3006,8 @@
val grandChild1 = rule.onNodeWithTag("grandChild1").fetchSemanticsNode()
val grandChild2 = rule.onNodeWithTag("grandChild2").fetchSemanticsNode()
rule.runOnIdle {
- grandChild1.layoutNode.innerCoordinator.detach()
- grandChild2.layoutNode.innerCoordinator.detach()
+ grandChild1.layoutNode.innerCoordinator.onRelease()
+ grandChild2.layoutNode.innerCoordinator.onRelease()
}
rule.runOnIdle {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
index 9e3fc69..0bab1a5 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
@@ -1305,7 +1305,10 @@
assertThat(activeLayers).isEqualTo(1)
- root.removeAll()
+ val node = root.children[0]
+ root.removeAt(0, 1)
+ // in the real composition after removing the node onRelease() will be called as well
+ node.onRelease()
delegate.measureAndLayout()
assertThat(activeLayers).isEqualTo(0)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
new file mode 100644
index 0000000..1352f5f
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
@@ -0,0 +1,654 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalComposeUiApi::class)
+
+package androidx.compose.ui.modifier
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReusableContent
+import androidx.compose.runtime.ReusableContentHost
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ObserverNode
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ModifierNodeReuseAndDeactivationTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun reusingCallsResetOnModifier() {
+ var reuseKey by mutableStateOf(0)
+
+ var resetCalls = 0
+
+ rule.setContent {
+ ReusableContent(reuseKey) {
+ TestLayout(onReset = { resetCalls++ })
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(resetCalls).isEqualTo(0)
+ reuseKey = 1
+ }
+
+ rule.runOnIdle {
+ assertThat(resetCalls).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun nodeIsNotRecreatedWhenReused() {
+ var reuseKey by mutableStateOf(0)
+
+ var createCalls = 0
+
+ rule.setContent {
+ ReusableContent(reuseKey) {
+ TestLayout(onCreate = { createCalls++ })
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(createCalls).isEqualTo(1)
+ reuseKey = 1
+ }
+
+ rule.runOnIdle {
+ assertThat(createCalls).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun resetIsCalledWhenContentIsDeactivated() {
+ var active by mutableStateOf(true)
+ var resetCalls = 0
+
+ rule.setContent {
+ ReusableContentHost(active) {
+ ReusableContent(0) {
+ TestLayout(onReset = { resetCalls++ })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(resetCalls).isEqualTo(0)
+ active = false
+ }
+
+ rule.runOnIdle {
+ assertThat(resetCalls).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun resetIsCalledAgainWhenContentIsReactivated() {
+ var active by mutableStateOf(true)
+ var resetCalls = 0
+
+ rule.setContent {
+ ReusableContentHost(active) {
+ ReusableContent(0) {
+ TestLayout(onReset = { resetCalls++ })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ active = false
+ }
+
+ rule.runOnIdle {
+ active = true
+ }
+
+ rule.runOnIdle {
+ assertThat(resetCalls).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun updateIsNotCalledWhenReusedWithTheSameParams() {
+ var reuseKey by mutableStateOf(0)
+ var updateCalls = 0
+
+ rule.setContent {
+ ReusableContent(reuseKey) {
+ TestLayout(
+ key = 1,
+ onUpdate = { updateCalls++ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(updateCalls).isEqualTo(0)
+ reuseKey++
+ }
+
+ rule.runOnIdle {
+ assertThat(updateCalls).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun updateIsCalledWhenReusedWithDifferentParam() {
+ var reuseKey by mutableStateOf(0)
+ var updateCalls = 0
+
+ rule.setContent {
+ ReusableContent(reuseKey) {
+ TestLayout(
+ key = reuseKey,
+ onUpdate = { updateCalls++ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(updateCalls).isEqualTo(0)
+ reuseKey++
+ }
+
+ rule.runOnIdle {
+ assertThat(updateCalls).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun nodesAreDetachedWhenReused() {
+ var reuseKey by mutableStateOf(0)
+
+ var onResetCalls = 0
+ var onAttachCalls = 0
+ var onResetCallsWhenDetached: Int? = null
+
+ rule.setContent {
+ ReusableContent(reuseKey) {
+ TestLayout(
+ onAttach = { onAttachCalls++ },
+ onReset = { onResetCalls++ },
+ onDetach = { onResetCallsWhenDetached = onResetCalls }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(onAttachCalls).isEqualTo(1)
+ assertThat(onResetCallsWhenDetached).isNull()
+ reuseKey = 1
+ }
+
+ rule.runOnIdle {
+ assertThat(onResetCalls).isEqualTo(1)
+ // makes sure onReset is called before detach:
+ assertThat(onResetCallsWhenDetached).isEqualTo(1)
+ assertThat(onAttachCalls).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun nodesAreDetachedAndAttachedWhenDeactivatedAndReactivated() {
+ var active by mutableStateOf(true)
+
+ var onResetCalls = 0
+ var onAttachCalls = 0
+ var onResetCallsWhenDetached: Int? = null
+
+ rule.setContent {
+ ReusableContentHost(active) {
+ ReusableContent(0) {
+ TestLayout(
+ onAttach = { onAttachCalls++ },
+ onReset = { onResetCalls++ },
+ onDetach = { onResetCallsWhenDetached = onResetCalls }
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(onAttachCalls).isEqualTo(1)
+ assertThat(onResetCallsWhenDetached).isNull()
+ active = false
+ }
+
+ rule.runOnIdle {
+ assertThat(onResetCalls).isEqualTo(1)
+ // makes sure onReset is called before detach:
+ assertThat(onResetCallsWhenDetached).isEqualTo(1)
+ assertThat(onAttachCalls).isEqualTo(1)
+ active = true
+ }
+
+ rule.runOnIdle {
+ assertThat(onAttachCalls).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun reusingStatelessModifierNotCausingInvalidation() {
+ var active by mutableStateOf(true)
+ var reuseKey by mutableStateOf(0)
+
+ var invalidations = 0
+ val onInvalidate: () -> Unit = {
+ invalidations++
+ }
+
+ rule.setContent {
+ ReusableContentHost(active) {
+ ReusableContent(reuseKey) {
+ Layout(
+ modifier = StatelessModifierElement(onInvalidate),
+ measurePolicy = MeasurePolicy
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(1)
+ active = false
+ }
+
+ rule.runOnIdle {
+ active = true
+ reuseKey = 1
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun reusingStatelessModifierWithUpdatedInputCausingInvalidation() {
+ var active by mutableStateOf(true)
+ var reuseKey by mutableStateOf(0)
+ var size by mutableStateOf(10)
+
+ var invalidations = 0
+ val onInvalidate: () -> Unit = {
+ invalidations++
+ }
+
+ rule.setContent {
+ ReusableContentHost(active) {
+ ReusableContent(reuseKey) {
+ Layout(
+ modifier = StatelessModifierElement(onInvalidate, size),
+ measurePolicy = MeasurePolicy
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(1)
+ active = false
+ }
+
+ rule.runOnIdle {
+ active = true
+ reuseKey = 1
+ size = 20
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun reusingModifierCausingInvalidationOnDelegatedInnerNode() {
+ var reuseKey by mutableStateOf(0)
+
+ var resetCalls = 0
+ val onReset: () -> Unit = {
+ resetCalls++
+ }
+
+ rule.setContent {
+ ReusableContent(reuseKey) {
+ Layout(
+ modifier = DelegatingModifierElement(onReset),
+ measurePolicy = MeasurePolicy
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(resetCalls).isEqualTo(0)
+ reuseKey = 1
+ }
+
+ rule.runOnIdle {
+ assertThat(resetCalls).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun reusingModifierReadingStateInLayerBlock() {
+ var active by mutableStateOf(true)
+ var counter by mutableStateOf(0)
+
+ var invalidations = 0
+ val layerBlock: () -> Unit = {
+ // state read
+ counter.toString()
+ invalidations++
+ }
+
+ rule.setContent {
+ ReusableContentHost(active) {
+ ReusableContent(0) {
+ Layout(
+ modifier = LayerModifierElement(layerBlock),
+ measurePolicy = MeasurePolicy
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(1)
+ active = false
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(1)
+ counter++
+ }
+
+ rule.runOnIdle {
+ active = true
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(2)
+ counter++
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(3)
+ }
+ }
+
+ @Test
+ fun reusingModifierObservingState() {
+ var active by mutableStateOf(true)
+ var counter by mutableStateOf(0)
+
+ var invalidations = 0
+ val observedBlock: () -> Unit = {
+ // state read
+ counter.toString()
+ invalidations++
+ }
+
+ rule.setContent {
+ ReusableContentHost(active) {
+ ReusableContent(0) {
+ Layout(
+ modifier = ObserverModifierElement(observedBlock),
+ measurePolicy = MeasurePolicy
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(1)
+ active = false
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(1)
+ counter++
+ }
+
+ rule.runOnIdle {
+ active = true
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(2)
+ counter++
+ }
+
+ rule.runOnIdle {
+ assertThat(invalidations).isEqualTo(3)
+ }
+ }
+
+ @Test
+ fun reusingModifierLocalProviderAndConsumer() {
+ val key = modifierLocalOf { -1 }
+ var active by mutableStateOf(true)
+ var providedValue by mutableStateOf(0)
+
+ var receivedValue: Int? = null
+
+ rule.setContent {
+ ReusableContentHost(active) {
+ ReusableContent(0) {
+ Layout(
+ modifier = Modifier
+ .modifierLocalProvider(key) { providedValue }
+ .modifierLocalConsumer { receivedValue = key.current },
+ measurePolicy = MeasurePolicy
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(receivedValue).isEqualTo(0)
+ active = false
+ }
+
+ rule.runOnIdle {
+ providedValue = 1
+ }
+
+ rule.runOnIdle {
+ active = true
+ }
+
+ rule.runOnIdle {
+ assertThat(receivedValue).isEqualTo(1)
+ }
+ }
+}
+
+@Composable
+private fun TestLayout(
+ key: Any? = null,
+ onReset: () -> Unit = {},
+ onCreate: () -> Unit = {},
+ onUpdate: () -> Unit = {},
+ onDetach: () -> Unit = {},
+ onAttach: () -> Unit = {}
+) {
+ val currentOnReset by rememberUpdatedState(onReset)
+ val currentOnCreate by rememberUpdatedState(onCreate)
+ val currentOnUpdate by rememberUpdatedState(onUpdate)
+ val currentOnDetach by rememberUpdatedState(onDetach)
+ val currentOnAttach by rememberUpdatedState(onAttach)
+ Layout(
+ modifier = createModifier(
+ key = key,
+ onCreate = { currentOnCreate.invoke() },
+ onUpdate = { currentOnUpdate.invoke() },
+ onReset = { currentOnReset.invoke() },
+ onDetach = { currentOnDetach.invoke() },
+ onAttach = { currentOnAttach.invoke() },
+ ),
+ measurePolicy = MeasurePolicy
+ )
+}
+
+private fun createModifier(
+ key: Any? = null,
+ onCreate: () -> Unit = {},
+ onUpdate: () -> Unit = {},
+ onReset: () -> Unit = {},
+ onDetach: () -> Unit = {},
+ onAttach: () -> Unit = {},
+): Modifier {
+ return modifierElementOf(
+ key = key,
+ create = {
+ onCreate()
+ object : Modifier.Node() {
+ override fun onReset() = onReset()
+ override fun onAttach() = onAttach()
+ override fun onDetach() = onDetach()
+ }
+ },
+ update = {
+ onUpdate()
+ }
+ ) {}
+}
+
+private val MeasurePolicy = MeasurePolicy { _, _ ->
+ layout(100, 100) { }
+}
+
+private class StatelessModifierElement(
+ private val onInvalidate: () -> Unit,
+ private val size: Int = 10
+) : ModifierNodeElement<StatelessModifierElement.Node>(params = size, inspectorInfo = {}) {
+ override fun create() = Node(size, onInvalidate)
+
+ override fun update(node: Node) = node.also {
+ it.size = size
+ it.onMeasure = onInvalidate
+ }
+
+ class Node(var size: Int, var onMeasure: () -> Unit) : Modifier.Node(), LayoutModifierNode {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(Constraints.fixed(size, size))
+ onMeasure()
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ }
+}
+
+private class DelegatingModifierElement(
+ private val onDelegatedNodeReset: () -> Unit,
+) : ModifierNodeElement<DelegatingModifierElement.Node>(inspectorInfo = {}) {
+ override fun create() = Node(onDelegatedNodeReset)
+
+ override fun update(node: Node) = node.also {
+ it.onReset = onDelegatedNodeReset
+ }
+
+ class Node(var onReset: () -> Unit) : DelegatingNode() {
+ private val inner = delegated {
+ object : Modifier.Node() {
+ override fun onReset() {
+ this@Node.onReset.invoke()
+ }
+ }
+ }
+ }
+}
+
+private class LayerModifierElement(
+ private val layerBlock: () -> Unit,
+) : ModifierNodeElement<LayerModifierElement.Node>(inspectorInfo = {}) {
+ override fun create() = Node(layerBlock)
+
+ override fun update(node: Node) = node.also {
+ it.layerBlock = layerBlock
+ }
+
+ class Node(var layerBlock: () -> Unit) : Modifier.Node(), LayoutModifierNode {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.placeWithLayer(0, 0) {
+ layerBlock.invoke()
+ }
+ }
+ }
+ }
+}
+
+private class ObserverModifierElement(
+ private val observedBlock: () -> Unit,
+) : ModifierNodeElement<ObserverModifierElement.Node>(inspectorInfo = {}) {
+ override fun create() = Node(observedBlock)
+
+ override fun update(node: Node) = node.also {
+ it.observedBlock = observedBlock
+ }
+
+ class Node(var observedBlock: () -> Unit) : Modifier.Node(), ObserverNode {
+
+ override fun onAttach() {
+ observe()
+ }
+
+ private fun observe() {
+ observeReads {
+ observedBlock()
+ }
+ }
+
+ override fun onObservedReadsChanged() {
+ observe()
+ }
+ }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
index 0d873b0..99859f7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
@@ -175,11 +175,13 @@
internal var ownerScope: ModifierNodeOwnerScope? = null
internal var coordinator: NodeCoordinator? = null
private set
+ internal var insertedNodeAwaitingAttachForInvalidation = false
+ internal var updatedNodeAwaitingAttachForInvalidation = false
/**
- * Indicates that the node is attached and part of the tree. This will get set to true
- * right before [onAttach] is called, and set to false right after [onDetach] is called.
- *
- * A Node will never be attached more than once.
+ * Indicates that the node is attached to a [androidx.compose.ui.layout.Layout] which is
+ * part of the UI tree.
+ * This will get set to true right before [onAttach] is called, and set to false right
+ * after [onDetach] is called.
*
* @see onAttach
* @see onDetach
@@ -194,7 +196,7 @@
@Suppress("NOTHING_TO_INLINE")
internal inline fun isKind(kind: NodeKind<*>) = kindSet and kind.mask != 0
- internal fun attach() {
+ internal open fun attach() {
check(!isAttached)
check(coordinator != null)
isAttached = true
@@ -202,7 +204,7 @@
// TODO(lmr): run side effects?
}
- internal fun detach() {
+ internal open fun detach() {
check(isAttached)
check(coordinator != null)
onDetach()
@@ -211,13 +213,23 @@
// TODO(lmr): cancel jobs / side effects?
}
+ internal open fun reset() {
+ check(isAttached)
+ onReset()
+ }
+
/**
+ * Called when the node is attached to a [androidx.compose.ui.layout.Layout] which is
+ * part of the UI tree.
* When called, `node` is guaranteed to be non-null. You can call sideEffect,
* coroutineScope, etc.
*/
open fun onAttach() {}
/**
+ * Called when the node is not attached to a [androidx.compose.ui.layout.Layout] which is
+ * not a part of the UI tree anymore. Note that the node can be reattached again.
+ *
* This should be called right before the node gets removed from the list, so you should
* still be able to traverse inside of this method. Ideally we would not allow you to
* trigger side effects here.
@@ -225,6 +237,22 @@
open fun onDetach() {}
/**
+ * Called when the node is about to be moved to a pool of layouts ready to be reused.
+ * For example it happens when the node is part of the item of LazyColumn after this item
+ * is scrolled out of the viewport. This means this node could be in future reused for a
+ * [androidx.compose.ui.layout.Layout] displaying a semantically different content when
+ * the list will be populating a new item.
+ *
+ * Use this callback to reset some local item specific state, like "is my component focused".
+ *
+ * This callback is called while the node is attached. Right after this callback the node
+ * will be detached and later reattached when reused.
+ *
+ * @sample androidx.compose.ui.samples.ModifierNodeResetSample
+ */
+ open fun onReset() {}
+
+ /**
* This can be called to register [effect] as a function to be executed after all of the
* changes to the tree are applied.
*
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
index 61519df..b73a8cb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
@@ -54,14 +54,21 @@
if (previousFocusState != focusState) refreshFocusEventNodes()
}
- internal fun onRemoved() {
+ /**
+ * Clears focus if this focus target has it.
+ */
+ override fun onReset() {
when (focusState) {
// Clear focus from the current FocusTarget.
// This currently clears focus from the entire hierarchy, but we can change the
// implementation so that focus is sent to the immediate focus parent.
Active, Captured -> requireOwner().focusOwner.clearFocus(force = true)
-
- ActiveParent, Inactive -> scheduleInvalidationForFocusEvents()
+ ActiveParent -> {
+ scheduleInvalidationForFocusEvents()
+ // This node might be reused, so reset the state to Inactive.
+ focusStateImpl = Inactive
+ }
+ Inactive -> scheduleInvalidationForFocusEvents()
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt
index a449536..620349c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt
@@ -63,7 +63,7 @@
val toUpdate = hashSetOf<BackwardsCompatNode>()
removed.forEachIndexed { i, layout ->
val key = removedLocal[i]
- if (layout.isAttached) {
+ if (layout.nodes.head.isAttached) {
// if the layout is still attached, that means that this provider got removed and
// there's possible some consumers below it that need to be updated
invalidateConsumersOfNodeForKey(layout.nodes.head, key, toUpdate)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
index af4d5d2..65f3f8e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
@@ -153,7 +153,9 @@
if (element is DrawCacheModifier) {
invalidateCache = true
}
- invalidateLayer()
+ if (!duringAttach) {
+ invalidateLayer()
+ }
}
if (isKind(Nodes.Layout)) {
val isChainUpdate = requireLayoutNode().nodes.tail.isAttached
@@ -163,8 +165,10 @@
coordinator.layoutModifierNode = this
coordinator.onLayoutModifierNodeChanged()
}
- invalidateLayer()
- requireLayoutNode().invalidateMeasurements()
+ if (!duringAttach) {
+ invalidateLayer()
+ requireLayoutNode().invalidateMeasurements()
+ }
}
if (element is RemeasurementModifier) {
element.onRemeasurementAvailable(this)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
index 3b5ac23..8bbe3a8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
@@ -80,16 +80,21 @@
}
}
- override fun onAttach() {
- super.onAttach()
+ override fun attach() {
+ super.attach()
forEachDelegate {
it.updateCoordinator(coordinator)
it.attach()
}
}
- override fun onDetach() {
+ override fun detach() {
forEachDelegate { it.detach() }
- super.onDetach()
+ super.detach()
+ }
+
+ override fun reset() {
+ super.reset()
+ forEachDelegate { it.reset() }
}
}
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 a410193..2c425f0 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
@@ -15,6 +15,7 @@
*/
package androidx.compose.ui.node
+import androidx.compose.runtime.ComposeNodeLifecycleCallback
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -73,7 +74,11 @@
private val isVirtual: Boolean = false,
// The unique semantics ID that is used by all semantics modifiers attached to this LayoutNode.
override val semanticsId: Int = generateSemanticsId()
-) : Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode,
+) : ComposeNodeLifecycleCallback,
+ Remeasurement,
+ OwnerScope,
+ LayoutInfo,
+ ComposeUiNode,
Owner.OnLayoutCompletedListener {
val isPlacedInLookahead: Boolean?
@@ -400,7 +405,7 @@
invalidateMeasurements()
parent?.invalidateMeasurements()
- forEachCoordinatorIncludingInner { it.attach() }
+ forEachCoordinatorIncludingInner { it.onLayoutNodeAttach() }
onAttach?.invoke(owner)
invalidateFocusOnAttach()
@@ -425,7 +430,6 @@
}
layoutDelegate.resetAlignmentLines()
onDetach?.invoke(owner)
- forEachCoordinatorIncludingInner { it.detach() }
@OptIn(ExperimentalComposeUiApi::class)
if (outerSemantics != null) {
@@ -748,7 +752,6 @@
*/
override var modifier: Modifier = Modifier
set(value) {
- if (value == field) return
require(!isVirtual || modifier === Modifier) {
"Modifiers are not supported on virtual LayoutNodes"
}
@@ -764,6 +767,10 @@
layoutDelegate.updateParentData()
}
+ private fun resetModifierState() {
+ nodes.resetState()
+ }
+
internal fun invalidateParentData() {
layoutDelegate.invalidateParentData()
}
@@ -1346,6 +1353,26 @@
override val parentInfo: LayoutInfo?
get() = parent
+ private var deactivated = false
+
+ override fun onReuse() {
+ if (deactivated) {
+ deactivated = false
+ // we don't need to reset state as it was done when deactivated
+ } else {
+ resetModifierState()
+ }
+ }
+
+ override fun onDeactivate() {
+ deactivated = true
+ resetModifierState()
+ }
+
+ override fun onRelease() {
+ forEachCoordinatorIncludingInner { it.onRelease() }
+ }
+
internal companion object {
private val ErrorMeasurePolicy: NoIntrinsicsMeasurePolicy =
object : NoIntrinsicsMeasurePolicy(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
index 6116c55..a936861 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
@@ -56,7 +56,7 @@
) : Modifier.Element, InspectorValueInfo(inspectorInfo) {
/**
* This will be called the first time the modifier is applied to the Layout and it should
- * construct and return the correspoding [Modifier.Node] instance.
+ * construct and return the corresponding [Modifier.Node] instance.
*/
abstract fun create(): N
@@ -85,7 +85,7 @@
*
* @param key An object used to determine whether or not the created node should be updated or not.
* @param create The initial creation of the node. This will be called the first time the modifier
- * is applied to the Layout and it should construct the correspoding [Modifier.Node] instance,
+ * is applied to the Layout and it should construct the corresponding [Modifier.Node] instance,
* referencing any captured inputs necessary.
* @param update Called when a modifier is applied to a Layout whose [key] have changed from the
* previous application. This lambda will have the current node instance passed in as a parameter,
@@ -123,7 +123,7 @@
* which accepts a "params" and "update" parameter.
*
* @param create The initial creation of the node. This will be called the first time the modifier
- * is applied to the Layout and it should construct the correspoding [Modifier.Node] instance
+ * is applied to the Layout and it should construct the corresponding [Modifier.Node] instance
* @param definitions This lambda will construct a debug-only set of information for use with
* tooling.
*
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index e1d54e7..e61d347 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -97,7 +97,7 @@
// to avoid allocating vectors every time modifier is set, we have two vectors that we
// reuse over time. Since the common case is the modifier chains will be of equal length,
// these vectors should be sized appropriately
- val before = current ?: mutableVectorOf()
+ val before = current ?: MutableVector(capacity = 0)
val after = m.fillVector(buffer ?: mutableVectorOf())
if (after.size == before.size) {
// assume if the sizes are the same, that we are in a common case of no structural
@@ -111,7 +111,7 @@
while (node != null && i >= 0) {
val prev = before[i]
val next = after[i]
- when (reuseActionForModifiers(prev, next)) {
+ when (actionForModifiers(prev, next)) {
ActionReplace -> {
// TODO(lmr): we could avoid running the diff if i = 0, since that would
// always be simple remove + insert
@@ -128,8 +128,6 @@
// reuse the node but also update it
val beforeUpdate = node
node = updateNodeAndReplaceIfNeeded(prev, next, beforeUpdate)
- // if the node is new, we need to run attach on it
- attachNeeded = attachNeeded || beforeUpdate !== node
logger?.nodeUpdated(i, i, prev, next, beforeUpdate, node)
}
ActionReuse -> {
@@ -137,10 +135,14 @@
// no need to do anything, this is "the same" modifier
}
}
- i--
+ // if the node is new, we need to run attach on it
+ if (!node.isAttached) attachNeeded = true
+
aggregateChildKindSet = aggregateChildKindSet or node.kindSet
node.aggregateChildKindSet = aggregateChildKindSet
+
node = node.parent
+ i--
}
if (i > 0) {
@@ -178,6 +180,18 @@
node.aggregateChildKindSet = aggregateChildKindSet
i--
}
+ } else if (after.size == 0) {
+ // common case where we we are removing all the modifiers.
+ var i = before.size - 1
+ // for the linear traversal we want to start with the "unpadded" tail
+ var node: Modifier.Node? = tail.parent
+ while (node != null && i >= 0) {
+ logger?.nodeRemoved(i, before[i], node)
+ val parent = node.parent
+ detachAndRemoveNode(node)
+ node = parent
+ i--
+ }
} else {
attachNeeded = true
coordinatorSyncNeeded = true
@@ -202,6 +216,25 @@
}
}
+ internal fun resetState() {
+ val current = current
+ if (current == null) {
+ // We have no modifiers set so there is nothing to reset.
+ return
+ }
+ val size = current.size
+ var node: Modifier.Node? = tail.parent
+ var i = size - 1
+ while (node != null && i >= 0) {
+ if (node.isAttached) {
+ node.reset()
+ node.detach()
+ }
+ node = node.parent
+ i--
+ }
+ }
+
private fun syncCoordinators() {
var coordinator: NodeCoordinator = innerCoordinator
var node: Modifier.Node? = tail.parent
@@ -234,7 +267,19 @@
headToTail {
if (!it.isAttached) {
it.attach()
- if (performInvalidations) autoInvalidateInsertedNode(it)
+ if (performInvalidations) {
+ if (it.insertedNodeAwaitingAttachForInvalidation) {
+ autoInvalidateInsertedNode(it)
+ }
+ if (it.updatedNodeAwaitingAttachForInvalidation) {
+ autoInvalidateUpdatedNode(it)
+ }
+ }
+ // when we attach with performInvalidations == false no separate
+ // invalidations needed as the whole LayoutNode is attached to the tree.
+ // it will cause all the needed invalidations.
+ it.insertedNodeAwaitingAttachForInvalidation = false
+ it.updatedNodeAwaitingAttachForInvalidation = false
}
}
}
@@ -301,12 +346,14 @@
var after: MutableVector<Modifier.Element>,
) : DiffCallback {
override fun areItemsTheSame(oldIndex: Int, newIndex: Int): Boolean {
- return reuseActionForModifiers(before[oldIndex], after[newIndex]) != ActionReplace
+ return actionForModifiers(before[oldIndex], after[newIndex]) != ActionReplace
}
override fun insert(atIndex: Int, newIndex: Int) {
val child = node
node = createAndInsertNodeAsParent(after[newIndex], child)
+ check(!node.isAttached)
+ node.insertedNodeAwaitingAttachForInvalidation = true
logger?.nodeInserted(atIndex, newIndex, after[newIndex], child, node)
aggregateChildKindSet = aggregateChildKindSet or node.kindSet
node.aggregateChildKindSet = aggregateChildKindSet
@@ -315,7 +362,7 @@
override fun remove(oldIndex: Int) {
node = node.parent!!
logger?.nodeRemoved(oldIndex, before[oldIndex], node)
- node = disposeAndRemoveNode(node)
+ node = detachAndRemoveNode(node)
}
override fun same(oldIndex: Int, newIndex: Int) {
@@ -427,7 +474,7 @@
return next
}
- private fun disposeAndRemoveNode(node: Modifier.Node): Modifier.Node {
+ private fun detachAndRemoveNode(node: Modifier.Node): Modifier.Node {
if (node.isAttached) {
// for removing nodes, we always do the autoInvalidateNode call,
// regardless of whether or not it was a ModifierNodeElement with autoInvalidate
@@ -474,6 +521,8 @@
}
else -> BackwardsCompatNode(element)
}
+ check(!node.isAttached)
+ node.insertedNodeAwaitingAttachForInvalidation = true
return insertParent(node, child)
}
@@ -503,29 +552,31 @@
private fun updateNodeAndReplaceIfNeeded(
prev: Modifier.Element,
next: Modifier.Element,
- node: Modifier.Node,
+ node: Modifier.Node
): Modifier.Node {
when {
prev is ModifierNodeElement<*> && next is ModifierNodeElement<*> -> {
val updated = next.updateUnsafe(node)
if (updated !== node) {
+ check(!updated.isAttached)
+ updated.insertedNodeAwaitingAttachForInvalidation = true
// if a new instance is returned, we want to detach the old one
if (node.isAttached) {
autoInvalidateRemovedNode(node)
node.detach()
}
- val result = replaceNode(node, updated)
- if (node.isAttached) {
- autoInvalidateInsertedNode(updated)
- }
- return result
+ return replaceNode(node, updated)
} else {
// the node was updated. we are done.
- if (next.autoInvalidate && updated.isAttached) {
- // the modifier element is labeled as "auto invalidate", which means
- // that since the node was updated, we need to invalidate everything
- // relevant to it.
- autoInvalidateUpdatedNode(updated)
+ if (next.autoInvalidate) {
+ if (updated.isAttached) {
+ // the modifier element is labeled as "auto invalidate", which means
+ // that since the node was updated, we need to invalidate everything
+ // relevant to it.
+ autoInvalidateUpdatedNode(updated)
+ } else {
+ updated.updatedNodeAwaitingAttachForInvalidation = true
+ }
}
return updated
}
@@ -535,6 +586,8 @@
// We always autoInvalidate BackwardsCompatNode.
if (node.isAttached) {
autoInvalidateUpdatedNode(node)
+ } else {
+ node.updatedNodeAwaitingAttachForInvalidation = true
}
return node
}
@@ -661,7 +714,7 @@
* 2. if modifiers are same class, we REUSE and UPDATE
* 3. else REPLACE (NO REUSE, NO UPDATE)
*/
-internal fun reuseActionForModifiers(prev: Modifier.Element, next: Modifier.Element): Int {
+internal fun actionForModifiers(prev: Modifier.Element, next: Modifier.Element): Int {
return if (prev == next)
ActionReuse
else if (areObjectsOfSameType(prev, next))
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 01cc2c9..19ff440 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -84,6 +84,8 @@
override val coordinates: LayoutCoordinates
get() = this
+ private var released = false
+
private fun headNode(includeTail: Boolean): Modifier.Node? {
return if (layoutNode.outerCoordinator === this) {
layoutNode.nodes.head
@@ -158,7 +160,7 @@
get() = _measureResult != null
override val isAttached: Boolean
- get() = tail.isAttached
+ get() = !released && layoutNode.isAttached
private var _measureResult: MeasureResult? = null
override var measureResult: MeasureResult
@@ -926,30 +928,19 @@
}
/**
- * Attaches the [NodeCoordinator] and its wrapped [NodeCoordinator] to an active
- * LayoutNode.
- *
* This will be called when the [LayoutNode] associated with this [NodeCoordinator] is
* attached to the [Owner].
- *
- * It is also called whenever the modifier chain is replaced and the [NodeCoordinator]s are
- * recreated.
*/
- open fun attach() {
+ fun onLayoutNodeAttach() {
onLayerBlockUpdated(layerBlock)
}
/**
- * Detaches the [NodeCoordinator] and its wrapped [NodeCoordinator] from an active
- * LayoutNode.
- *
* This will be called when the [LayoutNode] associated with this [NodeCoordinator] is
- * detached from the [Owner].
- *
- * It is also called whenever the modifier chain is replaced and the [NodeCoordinator]s are
- * recreated.
+ * released or when the [NodeCoordinator] is released (will not be used anymore).
*/
- open fun detach() {
+ fun onRelease() {
+ released = true
if (layer != null) {
onLayerBlockUpdated(null)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index 595c6b6..c57a109 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -210,7 +210,7 @@
node.invalidateMeasurements()
if (phase == Removed) {
val coordinator = node.requireCoordinator(Nodes.Layout)
- coordinator.detach()
+ coordinator.onRelease()
}
}
if (node.isKind(Nodes.GlobalPositionAware) && node is GlobalPositionAwareModifierNode) {
@@ -227,7 +227,9 @@
}
if (node.isKind(Nodes.FocusTarget) && node is FocusTargetModifierNode) {
when (phase) {
- Removed -> node.onRemoved()
+ // when we previously had focus target modifier on a node and then this modifier
+ // is removed we need to notify the focus tree about so the focus state is reset.
+ Removed -> node.onReset()
else -> node.requireOwner().focusOwner.scheduleInvalidation(node)
}
}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index 84732ff..5b7e198 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -280,9 +280,7 @@
private fun changeModifier(modifier: Modifier) {
with(layoutNode) {
- if (isAttached) { forEachNodeCoordinator { it.detach() } }
this.modifier = modifier
- if (isAttached) { forEachNodeCoordinator { it.attach() } }
owner?.onEndApplyChanges()
}
}
@@ -314,9 +312,13 @@
layoutNode: LayoutNode,
affectsLookahead: Boolean,
forceRequest: Boolean
- ) {}
- override fun onAttach(node: LayoutNode) = node.forEachNodeCoordinator { it.attach() }
- override fun onDetach(node: LayoutNode) = node.forEachNodeCoordinator { it.detach() }
+ ) {
+ }
+
+ override fun onAttach(node: LayoutNode) =
+ node.forEachNodeCoordinator { it.onLayoutNodeAttach() }
+
+ override fun onDetach(node: LayoutNode) {}
override val root: LayoutNode
get() = TODO("Not yet implemented")
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index d7fa104..3f54712a 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -2762,12 +2762,11 @@
* </p>
*
* @see AccessibilityEventCompat#getContentChangeTypes for all content change types.
- * @param minMillisBetweenContentChanges the minimum duration between content change events.
+ * @param duration the minimum duration between content change events.
*/
- public void setMinMillisBetweenContentChanges(int minMillisBetweenContentChanges) {
+ public void setMinMillisBetweenContentChanges(int duration) {
if (Build.VERSION.SDK_INT >= 19) {
- Api19Impl.getExtras(mInfo).putInt(MIN_MILLIS_BETWEEN_CONTENT_CHANGES_KEY,
- minMillisBetweenContentChanges);
+ Api19Impl.getExtras(mInfo).putInt(MIN_MILLIS_BETWEEN_CONTENT_CHANGES_KEY, duration);
}
}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
index c0b0036..9dc8728 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
@@ -224,8 +224,8 @@
@DiagnosticCode public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4;
/**
- * Indicates that the profile is compressed and a newer version of bundletool needs to be used
- * to build the app.
+ * Indicates that the profile is compressed and a version of bundletool newer than 1.13.2
+ * needs to be used to build the app.
*/
@DiagnosticCode public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5;
diff --git a/webkit/webkit/build.gradle b/webkit/webkit/build.gradle
index bb84e97..0cc7c77 100644
--- a/webkit/webkit/build.gradle
+++ b/webkit/webkit/build.gradle
@@ -72,4 +72,5 @@
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "The WebView Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions."
+ additionalDeviceTestApkKeys.add("chrome")
}