Merge "Migrate additional samples to new resolution method" into androidx-main
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
index c82ead6..d184d2f 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
@@ -20,7 +20,7 @@
// Minimum AGP version required
internal val MIN_AGP_VERSION_REQUIRED_INCLUSIVE = AndroidPluginVersion(8, 0, 0)
-internal val MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE = AndroidPluginVersion(8, 5, 0).alpha(1)
+internal val MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE = AndroidPluginVersion(8, 6, 0).alpha(1)
// Prefix for the build type baseline profile
internal const val BUILD_TYPE_BASELINE_PROFILE_PREFIX = "nonMinified"
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
index 0c71b29..65b2efc 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
@@ -16,7 +16,6 @@
package androidx.benchmark
-import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SmallTest
@@ -39,14 +38,11 @@
val state = BenchmarkState(config)
var count = 0
while (state.keepRunning()) {
- if (Build.VERSION.SDK_INT < 21) {
- // This spin loop works around an issue where on Mako API 17, nanoTime is only
- // precise to 30us. A more ideal fix might introduce an automatic divisor to
- // WarmupManager when the duration values it sees are 0, but this is simple.
- val start = System.nanoTime()
- @Suppress("ControlFlowWithEmptyBody")
- while (System.nanoTime() == start) {}
- }
+ // This spin loop works around an issue where nanoTime is only precise to 30us on some
+ // devices. This was reproduced on api 17 and emulators api 33. (b/331226761)
+ val start = System.nanoTime()
+ @Suppress("ControlFlowWithEmptyBody")
+ while (System.nanoTime() == start) {}
count++
}
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt
index f68f14d..ac0aee1 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt
@@ -18,15 +18,25 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import kotlin.test.assertFailsWith
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
-public class MetricResultTest {
+class MetricResultTest {
@Test
- public fun repeat() {
+ fun constructorThrowsIfEmpty() {
+ val exception = assertFailsWith<IllegalArgumentException> {
+ MetricResult("test", emptyList())
+ }
+
+ assertEquals("At least one result is necessary, 0 found for test.", exception.message!!)
+ }
+
+ @Test
+ fun repeat() {
val metricResult = MetricResult("test", listOf(10.0, 10.0, 10.0, 10.0))
assertEquals(10.0, metricResult.min, 0.0)
assertEquals(10.0, metricResult.max, 0.0)
@@ -39,7 +49,7 @@
}
@Test
- public fun one() {
+ fun one() {
val metricResult = MetricResult("test", listOf(10.0))
assertEquals(10.0, metricResult.min, 0.0)
assertEquals(10.0, metricResult.max, 0.0)
@@ -52,7 +62,7 @@
}
@Test
- public fun simple() {
+ fun simple() {
val metricResult = MetricResult("test", (0..100).map { it.toDouble() })
assertEquals(50.0, metricResult.median, 0.0)
assertEquals(100.0, metricResult.max, 0.0)
@@ -65,7 +75,7 @@
}
@Test
- public fun lerp() {
+ fun lerp() {
assertEquals(MetricResult.lerp(0.0, 1000.0, 0.5), 500.0, 0.0)
assertEquals(MetricResult.lerp(0.0, 1000.0, 0.75), 750.0, 0.0)
assertEquals(MetricResult.lerp(0.0, 1000.0, 0.25), 250.0, 0.0)
@@ -73,7 +83,7 @@
}
@Test
- public fun getPercentile() {
+ fun getPercentile() {
(0..100).forEach {
assertEquals(
it.toDouble(),
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt
index 3af2d32..357557a 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt
@@ -47,7 +47,7 @@
init {
val values = data.sorted()
val size = values.size
- require(size >= 1) { "At least one result is necessary." }
+ require(size >= 1) { "At least one result is necessary, $size found for $name." }
val mean: Double = data.average()
min = values.first()
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
index 50b1662..01c4831 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
@@ -17,8 +17,10 @@
package androidx.benchmark.macro
import android.annotation.SuppressLint
+import android.content.Intent
import androidx.annotation.RequiresApi
import androidx.benchmark.DeviceInfo
+import androidx.benchmark.json.BenchmarkData
import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -85,6 +87,38 @@
assertTrue(exception.message!!.contains("Require iterations > 0"))
}
+ @Test
+ fun macrobenchmarkWithStartupMode_noMethodTrace() {
+ val result = macrobenchmarkWithStartupMode(
+ uniqueName = "uniqueName", // ignored, uniqueness not important
+ className = "className",
+ testName = "testName",
+ packageName = Packages.TARGET,
+ metrics = listOf(StartupTimingMetric()),
+ compilationMode = CompilationMode.Ignore(),
+ iterations = 1,
+ startupMode = StartupMode.COLD,
+ perfettoConfig = null,
+ setupBlock = {},
+ measureBlock = {
+ startActivityAndWait(
+ Intent(
+ "androidx.benchmark.integration.macrobenchmark.target" +
+ ".TRIVIAL_STARTUP_ACTIVITY"
+ )
+ )
+ }
+ )
+ assertEquals(
+ 1,
+ result.profilerOutputs!!.size
+ )
+ assertEquals(
+ result.profilerOutputs!!.single().type,
+ BenchmarkData.TestResult.ProfilerOutput.Type.PerfettoTrace
+ )
+ }
+
enum class Block { Setup, Measure }
@RequiresApi(29)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt
index 65ada92..e15e059 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt
@@ -30,4 +30,11 @@
* Preferably use this app/package if not killing/compiling target.
*/
const val TEST = "androidx.benchmark.macro.test"
+
+ /**
+ * Package not present on device.
+ *
+ * Used to validate behavior when package can't be found.
+ */
+ const val MISSING = "not.real.fake.package"
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
index 055ccfd..b299dde 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
@@ -17,43 +17,82 @@
package androidx.benchmark.macro
import android.os.Build
-import androidx.benchmark.junit4.PerfettoTraceRule
-import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
+import kotlin.test.assertContains
import kotlin.test.assertNull
-import org.junit.Rule
+import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class ProfileInstallBroadcastTest {
- @OptIn(ExperimentalPerfettoCaptureApi::class)
- @get:Rule
- val perfettoTraceRule = PerfettoTraceRule()
-
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
@Test
fun installProfile() {
assertNull(ProfileInstallBroadcast.installProfile(Packages.TARGET))
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+ @Test
+ fun installProfile_missing() {
+ val errorString = ProfileInstallBroadcast.installProfile(Packages.MISSING)
+ assertNotNull(errorString)
+ assertContains(
+ errorString!!,
+ "The baseline profile install broadcast was not received"
+ )
+ }
+
@Test
fun skipFileOperation() {
assertNull(ProfileInstallBroadcast.skipFileOperation(Packages.TARGET, "WRITE_SKIP_FILE"))
assertNull(ProfileInstallBroadcast.skipFileOperation(Packages.TARGET, "DELETE_SKIP_FILE"))
}
+ @Test
+ fun skipFileOperation_missing() {
+ ProfileInstallBroadcast.skipFileOperation(Packages.MISSING, "WRITE_SKIP_FILE").apply {
+ assertNotNull(this)
+ assertContains(this!!, "The baseline profile skip file broadcast was not received")
+ }
+ ProfileInstallBroadcast.skipFileOperation(Packages.MISSING, "DELETE_SKIP_FILE").apply {
+ assertNotNull(this)
+ assertContains(this!!, "The baseline profile skip file broadcast was not received")
+ }
+ }
+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
@Test
fun saveProfile() {
assertNull(ProfileInstallBroadcast.saveProfile(Packages.TARGET))
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+ @Test
+ fun saveProfile_missing() {
+ val errorString = ProfileInstallBroadcast.saveProfile(Packages.MISSING)
+ assertNotNull(errorString)
+ assertContains(errorString!!, "The save profile broadcast event was not received")
+ }
+
@Test
fun dropShaderCache() {
assertNull(ProfileInstallBroadcast.dropShaderCache(Packages.TARGET))
}
+
+ @Test
+ fun dropShaderCache_missing() {
+ val errorString = ProfileInstallBroadcast.dropShaderCache(Packages.MISSING)
+ assertNotNull(errorString)
+ assertContains(errorString!!, "The DROP_SHADER_CACHE broadcast was not received")
+
+ // validate extra instructions
+ assertContains(
+ errorString,
+ "verify: 1) androidx.profileinstaller.ProfileInstallReceiver appears unobfuscated"
+ )
+ }
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index ec88304..a16bfd5 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -230,10 +230,6 @@
// Capture if the app being benchmarked is a system app.
scope.isSystemApp = applicationInfo.isSystemApp()
- if (requestMethodTracing) {
- scope.startMethodTracing()
- }
-
// Ensure the device is awake
scope.device.wakeUp()
@@ -309,7 +305,9 @@
it.start()
}
}
- scope.startMethodTracing()
+ if (requestMethodTracing) {
+ scope.startMethodTracing()
+ }
trace("measureBlock") {
measureBlock(scope)
}
@@ -318,7 +316,9 @@
metrics.forEach {
it.stop()
}
- methodTracingResultFiles += scope.stopMethodTracing()
+ if (requestMethodTracing) {
+ methodTracingResultFiles += scope.stopMethodTracing()
+ }
}
}
}!!
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
index bd622f0..e51872e 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
@@ -187,10 +187,8 @@
// Use an explicit broadcast given the app was force-stopped.
val action = "androidx.profileinstaller.action.BENCHMARK_OPERATION"
val operationKey = "EXTRA_BENCHMARK_OPERATION"
- val result = Shell.amBroadcast(
- "-a $action -e $operationKey $operation $packageName/$receiverName"
- )
- return when (result) {
+ val broadcastArguments = "-a $action -e $operationKey $operation $packageName/$receiverName"
+ return when (val result = Shell.amBroadcast(broadcastArguments)) {
null, 0, 16 /* BENCHMARK_OPERATION_UNKNOWN */ -> {
// 0 is returned by the platform by default, and also if no broadcast receiver
// receives the broadcast.
@@ -201,7 +199,12 @@
"This most likely means that the `androidx.profileinstaller` library " +
"used by the target apk is old. Please use `1.3.0-alpha02` or newer. " +
"For more information refer to the release notes at " +
- "https://developer.android.com/jetpack/androidx/releases/profileinstaller."
+ "https://developer.android.com/jetpack/androidx/releases/profileinstaller. " +
+ "If you are already using androidx.profileinstaller library and still seeing " +
+ "error, verify: 1) androidx.profileinstaller.ProfileInstallReceiver appears " +
+ "unobfuscated in your APK's AndroidManifest and dex, and 2) the following " +
+ "command executes successfully (should print 14): " +
+ "adb shell am broadcast $broadcastArguments"
}
15 -> { // RESULT_BENCHMARK_OPERATION_FAILURE
"The $operation broadcast failed."
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt
new file mode 100644
index 0000000..9852727
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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(ExperimentalLibraryAbiReader::class)
+
+package androidx.binarycompatibilityvalidator
+
+import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+import org.jetbrains.kotlin.library.abi.LibraryAbi
+
+@Suppress("UNUSED_PARAMETER")
+@OptIn(ExperimentalLibraryAbiReader::class)
+class BinaryCompatibilityChecker(
+ private val newLibraryAbi: LibraryAbi,
+ private val oldLibraryAbi: LibraryAbi
+) {
+ fun checkBinariesAreCompatible() {
+ TODO()
+ }
+
+ companion object {
+ fun checkAllBinariesAreCompatible(
+ newLibraryAbis: Map<String, LibraryAbi>,
+ oldLibraryAbis: Map<String, LibraryAbi>
+ ) {
+ TODO()
+ }
+ }
+}
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt b/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
index f34133e..330676e 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
@@ -21,7 +21,6 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
import org.apache.tools.zip.ZipOutputStream
import org.gradle.api.Project
-import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.Usage
import org.gradle.api.file.FileTreeElement
@@ -29,12 +28,8 @@
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskProvider
import org.gradle.jvm.tasks.Jar
-import org.gradle.kotlin.dsl.findByType
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.named
-import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
-import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
/** Allow java and Android libraries to bundle other projects inside the project jar/aar. */
object BundleInsideHelper {
@@ -90,101 +85,6 @@
}
/**
- * Creates a configuration for the users to use that will be used bundle these dependency jars
- * inside of this project's jar.
- *
- * ```
- * dependencies {
- * bundleInside(project(":foo"))
- * debugBundleInside(project(path: ":bar", configuration: "someDebugConfiguration"))
- * releaseBundleInside(project(path: ":bar", configuration: "someReleaseConfiguration"))
- * }
- * ```
- *
- * @param from specifies from which package the rename should happen
- * @param to specifies to which package to put the renamed classes
- * @param dropResourcesWithSuffix used to drop Java resources if they match this suffix, null
- * means no filtering
- * @receiver the project that should bundle jars specified by these configurations
- */
- @JvmStatic
- fun Project.forInsideJar(from: String, to: String, dropResourcesWithSuffix: String?) {
- val bundle = createBundleConfiguration()
- val repackage =
- configureRepackageTaskForType(
- relocations = listOf(Relocation(from, to)),
- configuration = bundle,
- dropResourcesWithSuffix = dropResourcesWithSuffix
- )
- dependencies.add("compileOnly", files(repackage.flatMap { it.archiveFile }))
- dependencies.add("testImplementation", files(repackage.flatMap { it.archiveFile }))
-
- val jarTask = tasks.named("jar")
- jarTask.configure {
- it as Jar
- it.from(repackage.map { files(zipTree(it.archiveFile.get().asFile)) })
- }
- addArchivesToConfiguration("apiElements", jarTask)
- addArchivesToConfiguration("runtimeElements", jarTask)
- }
-
- private fun Project.addArchivesToConfiguration(
- configName: String,
- jarTask: TaskProvider<Task>
- ) {
- configurations.getByName(configName) {
- it.outgoing.artifacts.clear()
- it.outgoing.artifact(
- jarTask.flatMap { jarTask ->
- jarTask as Jar
- jarTask.archiveFile
- }
- )
- }
- }
-
- /**
- * KMP Version of [Project.forInsideJar]. See those docs for details.
- *
- * @param dropResourcesWithSuffix used to drop Java resources if they match this suffix,
- * * null means no filtering
- *
- * TODO(b/237104605): bundleInside is a global configuration. Should figure out how to make it
- * work properly with kmp and source sets so it can reside inside a sourceSet dependency.
- */
- @JvmStatic
- fun Project.forInsideJarKmp(from: String, to: String, dropResourcesWithSuffix: String?) {
- val kmpExtension =
- extensions.findByType<KotlinMultiplatformExtension>() ?: error("kmp only")
- val bundle = createBundleConfiguration()
- val repackage =
- configureRepackageTaskForType(
- relocations = listOf(Relocation(from, to)),
- configuration = bundle,
- dropResourcesWithSuffix = dropResourcesWithSuffix
- )
-
- // To account for KMP structure we need to find the jvm specific target
- // and add the repackaged archive files to only their compilations.
- val jvmTarget =
- kmpExtension.targets.firstOrNull { it.platformType == KotlinPlatformType.jvm }
- as? KotlinJvmTarget ?: error("cannot find jvm target")
- jvmTarget.compilations["main"].defaultSourceSet {
- dependencies { compileOnly(files(repackage.flatMap { it.archiveFile })) }
- }
- jvmTarget.compilations["test"].defaultSourceSet {
- dependencies { implementation(files(repackage.flatMap { it.archiveFile })) }
- }
- val jarTask = tasks.named(jvmTarget.artifactsTaskName)
- jarTask.configure {
- it as Jar
- it.from(repackage.map { files(zipTree(it.archiveFile.get().asFile)) })
- }
- addArchivesToConfiguration("jvmApiElements", jarTask)
- addArchivesToConfiguration("jvmRuntimeElements", jarTask)
- }
-
- /**
* Creates a configuration for users to use that will bundle the dependency jars
* inside of this lint check's jar. This is required because lintPublish does not currently
* support dependencies, so instead we need to bundle any dependencies with the lint jar
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/StreamSharingForceEnabledEffect.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/StreamSharingForceEnabledEffect.java
new file mode 100644
index 0000000..cb72a9f
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/StreamSharingForceEnabledEffect.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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.
+ */
+
+package androidx.camera.testing.impl;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraEffect;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.SurfaceOutput;
+import androidx.camera.core.SurfaceProcessor;
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.core.UseCaseGroup;
+import androidx.lifecycle.LifecycleOwner;
+
+/**
+ * An effect that is used to simulate the stream sharing is enabled automatically.
+ *
+ * <p>To simulate stream sharing is enabled automatically, create and add the effect to
+ * {@link UseCaseGroup.Builder#addEffect(CameraEffect)} and then bind UseCases via
+ * {@linkplain androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle(
+ * LifecycleOwner, CameraSelector, UseCaseGroup)}.
+ *
+ * <p>To test stream sharing with real effects, use {@link CameraEffect} API instead.
+ */
+public class StreamSharingForceEnabledEffect extends CameraEffect {
+
+ public StreamSharingForceEnabledEffect() {
+ super(PREVIEW | VIDEO_CAPTURE, TRANSFORMATION_PASSTHROUGH, command -> {
+ }, new SurfaceProcessor() {
+ @Override
+ public void onInputSurface(@NonNull SurfaceRequest request) {
+ request.willNotProvideSurface();
+ }
+
+ @Override
+ public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
+ surfaceOutput.close();
+ }
+ }, t -> {
+ });
+ }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
index d499582..bda76f0 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
@@ -42,12 +42,16 @@
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.camera.core.DynamicRange
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCaseGroup
import androidx.camera.core.impl.utils.TransformUtils.rotateSize
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.impl.AndroidUtil.isEmulator
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.StreamSharingForceEnabledEffect
+import androidx.camera.testing.impl.SurfaceTextureProvider
import androidx.camera.testing.impl.WakelockEmptyActivityRule
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.camera.video.internal.compat.quirk.DeviceQuirks
@@ -62,8 +66,8 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.junit.After
-import org.junit.Assume
import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -150,10 +154,10 @@
@Before
fun setUp() {
- Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
// Skip test for b/168175357
- Assume.assumeFalse(
+ assumeFalse(
"Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
)
@@ -172,7 +176,7 @@
// Ignore the unsupported Quality options
val videoCapabilities = Recorder.getVideoCapabilities(cameraInfo)
- Assume.assumeTrue(
+ assumeTrue(
"Camera ${cameraSelector.lensFacing} not support $quality, skip this test item.",
videoCapabilities.isQualitySupported(quality, dynamicRange)
)
@@ -194,10 +198,20 @@
fun qualityOptionCanRecordVideo_enableSurfaceProcessing() {
assumeSuccessfulSurfaceProcessing()
- testQualityOptionRecordVideo(enableSurfaceProcessing = true)
+ testQualityOptionRecordVideo(forceEnableSurfaceProcessing = true)
}
- private fun testQualityOptionRecordVideo(enableSurfaceProcessing: Boolean = false) {
+ @Test
+ fun qualityOptionCanRecordVideo_enableStreamSharing() {
+ assumeSuccessfulSurfaceProcessing()
+
+ testQualityOptionRecordVideo(forceEnableStreamSharing = true)
+ }
+
+ private fun testQualityOptionRecordVideo(
+ forceEnableSurfaceProcessing: Boolean = false,
+ forceEnableStreamSharing: Boolean = false,
+ ) {
// Skip for b/331618729
assumeFalse(
"Emulator API 28 crashes running this test.",
@@ -209,10 +223,12 @@
videoCapabilities.getProfiles(quality, dynamicRange)!!.defaultVideoProfile
val recorder = Recorder.Builder().setQualitySelector(QualitySelector.from(quality)).build()
val videoCapture = VideoCapture.Builder(recorder).apply {
- if (enableSurfaceProcessing) {
+ if (forceEnableSurfaceProcessing) {
setSurfaceProcessingForceEnabled()
}
}.build()
+ val preview = Preview.Builder().build()
+ assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture))
val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
val latchForRecordingStatus = CountDownLatch(5)
val latchForRecordingFinalized = CountDownLatch(1)
@@ -236,17 +252,29 @@
}
instrumentation.runOnMainSync {
+ preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
+ val useCaseGroup = UseCaseGroup.Builder().apply {
+ addUseCase(preview)
+ addUseCase(videoCapture)
+ if (forceEnableStreamSharing) {
+ addEffect(StreamSharingForceEnabledEffect())
+ }
+ }.build()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
- videoCapture,
+ useCaseGroup
)
}
- if (enableSurfaceProcessing) {
+ if (forceEnableSurfaceProcessing) {
// Ensure the surface processing is enabled.
assertThat(isSurfaceProcessingEnabled(videoCapture)).isTrue()
}
+ if (forceEnableStreamSharing) {
+ // Ensure the stream sharing is enabled.
+ assertThat(isStreamSharingEnabled(videoCapture)).isTrue()
+ }
// Act.
videoCapture.startVideoRecording(file, eventListener).use {
@@ -261,11 +289,17 @@
// Verify resolution.
val resolutionToVerify = Size(videoProfile.width, videoProfile.height)
val rotationDegrees = getRotationNeeded(videoCapture, cameraInfo)
+ // Skip verification when:
+ // * The device has extra cropping quirk. UseCase surface will be configured with a fixed
+ // resolution regardless of the preference.
+ // * The device has size can not encode quirk as the final resolution will be modified.
+ // * Flexible quality settings such as using HIGHEST and LOWEST. This is because the
+ // surface combination will affect the final resolution.
if (!hasExtraCroppingQuirk(implName) && !hasSizeCannotEncodeVideoQuirk(
resolutionToVerify,
rotationDegrees,
isSurfaceProcessingEnabled(videoCapture)
- )
+ ) && !isFlexibleQuality(quality)
) {
verifyVideoResolution(
context,
@@ -278,6 +312,9 @@
file.delete()
}
+ private fun isFlexibleQuality(quality: Quality) =
+ quality == Quality.HIGHEST || quality == Quality.LOWEST
+
private fun VideoCapture<Recorder>.startVideoRecording(
file: File,
eventListener: Consumer<VideoRecordEvent>
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 66b945f..7f0d5d9 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -40,6 +40,8 @@
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.UseCaseGroup
import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4
import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
@@ -53,6 +55,7 @@
import androidx.camera.testing.impl.AndroidUtil.skipVideoRecordingTestIfNotSupportedByEmulator
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.StreamSharingForceEnabledEffect
import androidx.camera.testing.impl.SurfaceTextureProvider
import androidx.camera.testing.impl.WakelockEmptyActivityRule
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
@@ -62,6 +65,7 @@
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
import androidx.core.util.Consumer
+import androidx.lifecycle.LifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -69,6 +73,7 @@
import androidx.test.rule.GrantPermissionRule
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.util.concurrent.ListenableFuture
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -91,7 +96,8 @@
class VideoRecordingTest(
private val implName: String,
private var cameraSelector: CameraSelector,
- private val cameraConfig: CameraXConfig
+ private val cameraConfig: CameraXConfig,
+ private val forceEnableStreamSharing: Boolean,
) {
@get:Rule
@@ -126,22 +132,38 @@
arrayOf(
"back+" + Camera2Config::class.simpleName,
CameraSelector.DEFAULT_BACK_CAMERA,
- Camera2Config.defaultConfig()
+ Camera2Config.defaultConfig(),
+ /*forceEnableStreamSharing=*/false,
),
arrayOf(
"front+" + Camera2Config::class.simpleName,
CameraSelector.DEFAULT_FRONT_CAMERA,
- Camera2Config.defaultConfig()
+ Camera2Config.defaultConfig(),
+ /*forceEnableStreamSharing=*/false,
+ ),
+ arrayOf(
+ "back+" + Camera2Config::class.simpleName + "+streamSharing",
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ Camera2Config.defaultConfig(),
+ /*forceEnableStreamSharing=*/true,
),
arrayOf(
"back+" + CameraPipeConfig::class.simpleName,
CameraSelector.DEFAULT_BACK_CAMERA,
- CameraPipeConfig.defaultConfig()
+ CameraPipeConfig.defaultConfig(),
+ /*forceEnableStreamSharing=*/false,
),
arrayOf(
"front+" + CameraPipeConfig::class.simpleName,
CameraSelector.DEFAULT_FRONT_CAMERA,
- CameraPipeConfig.defaultConfig()
+ CameraPipeConfig.defaultConfig(),
+ /*forceEnableStreamSharing=*/false,
+ ),
+ arrayOf(
+ "back+" + CameraPipeConfig::class.simpleName + "+streamSharing",
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ /*forceEnableStreamSharing=*/true,
),
)
}
@@ -151,7 +173,7 @@
private val context: Context = ApplicationProvider.getApplicationContext()
// TODO(b/278168212): Only SDR is checked by now. Need to extend to HDR dynamic ranges.
private val dynamicRange = DynamicRange.SDR
- private lateinit var cameraProvider: ProcessCameraProvider
+ private lateinit var cameraProvider: ProcessCameraProviderWrapper
private lateinit var lifecycleOwner: FakeLifecycleOwner
private lateinit var preview: Preview
private lateinit var cameraInfo: CameraInfo
@@ -202,7 +224,8 @@
skipVideoRecordingTestIfNotSupportedByEmulator()
ProcessCameraProvider.configureInstance(cameraConfig)
- cameraProvider = ProcessCameraProvider.getInstance(context).get()
+ cameraProvider =
+ ProcessCameraProviderWrapper(ProcessCameraProvider.getInstance(context).get())
lifecycleOwner = FakeLifecycleOwner()
lifecycleOwner.startAndResume()
@@ -1227,6 +1250,36 @@
assumeExtraCroppingQuirk(implName)
}
+ private inner class ProcessCameraProviderWrapper(val cameraProvider: ProcessCameraProvider) {
+
+ fun bindToLifecycle(
+ lifecycleOwner: LifecycleOwner,
+ cameraSelector: CameraSelector,
+ vararg useCases: UseCase
+ ): Camera {
+ if (useCases.isEmpty()) {
+ return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, *useCases)
+ }
+ val useCaseGroup = UseCaseGroup.Builder().apply {
+ useCases.forEach { useCase -> addUseCase(useCase) }
+ if (forceEnableStreamSharing) {
+ addEffect(StreamSharingForceEnabledEffect())
+ }
+ }.build()
+ return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)
+ }
+
+ fun unbind(vararg useCases: UseCase) {
+ cameraProvider.unbind(*useCases)
+ }
+
+ fun unbindAll() {
+ cameraProvider.unbindAll()
+ }
+
+ fun shutdownAsync(): ListenableFuture<Void> = cameraProvider.shutdownAsync()
+ }
+
private class ImageSavedCallback :
ImageCapture.OnImageSavedCallback {
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
index 34493db..fa4a65e 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
@@ -17,10 +17,11 @@
package androidx.compose.foundation.lazy.staggeredgrid
import androidx.compose.foundation.AutoTestFrameClock
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -34,11 +35,11 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-@OptIn(ExperimentalFoundationApi::class)
@MediumTest
@RunWith(Parameterized::class)
class LazyStaggeredGridScrollTest(
@@ -59,20 +60,23 @@
private var itemSizeDp = Dp.Unspecified
private val itemCount = 100
+ @Before
+ fun initSizes() {
+ itemSizeDp = with(rule.density) {
+ itemSizePx.toDp()
+ }
+ }
+
fun setContent(
containerSizePx: Int = itemSizePx * 5,
afterContentPaddingPx: Int = 0
) {
- itemSizeDp = with(rule.density) {
- itemSizePx.toDp()
- }
rule.setContent {
state = rememberLazyStaggeredGridState()
with(rule.density) {
TestContent(containerSizePx.toDp(), afterContentPaddingPx.toDp())
}
}
- rule.waitForIdle()
}
@Test
@@ -353,6 +357,41 @@
}
}
+ @Test
+ fun scrollBy_emptyItemWithSpacing() {
+ val state = LazyStaggeredGridState()
+ val spacingDp = with(rule.density) { 10.toDp() }
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 1,
+ state = state,
+ mainAxisSpacing = spacingDp,
+ modifier = Modifier.size(itemSizeDp),
+ ) {
+ item {
+ Box(Modifier.size(itemSizeDp).testTag("0").debugBorder())
+ }
+ item { } // empty item surrounded by spacings
+ item {
+ Box(Modifier.size(itemSizeDp).testTag("2").debugBorder())
+ }
+ }
+ }
+ rule.runOnIdle {
+ runBlocking(AutoTestFrameClock()) {
+ // empty item introduces two spacings 10 pixels each
+ // so after this operation item 2 is right at the edge of the viewport
+ state.scrollBy(20f)
+ // then we do some extra scrolling to make item 2 visible
+ state.scrollBy(20f)
+ }
+ }
+ rule.onNodeWithTag("2")
+ .assertMainAxisStartPositionInRootIsEqualTo(
+ itemSizeDp - with(rule.density) { 20.toDp() }
+ )
+ }
+
@Composable
private fun TestContent(containerSizeDp: Dp, afterContentPaddingDp: Dp) {
// |-|-|
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AndroidExternalSurfaceTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AndroidExternalSurfaceTest.kt
index d1097a6..4d0efc2 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AndroidExternalSurfaceTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AndroidExternalSurfaceTest.kt
@@ -273,6 +273,13 @@
}
@Test
+ @Ignore(
+ """Despite best efforts in screenshotToImage(), this test is too flaky currently.
+ |Since this test only tests that the `zOrder` parameter is properly passed
+ |to the underlying SurfaceView, we don't lose much by disabling it.
+ |This test should be more robust on API level 34 using the window
+ |screenshot API."""
+ )
fun testZOrderDefault() {
val latch = CountDownLatch(FrameCount)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index 7a75d11..a52b748 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -29,6 +29,7 @@
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
@@ -106,6 +107,8 @@
import com.google.common.truth.Truth.assertThat
import kotlin.reflect.KClass
import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.junit.After
@@ -1335,26 +1338,52 @@
private fun Modifier.dynamicPointerInputModifier(
enabled: Boolean,
key: Any? = Unit,
- onPress: () -> Unit = { },
+ onEnter: () -> Unit = { },
onMove: () -> Unit = { },
+ onPress: () -> Unit = { },
onRelease: () -> Unit = { },
- ) = if (enabled) {
+ onExit: () -> Unit = { },
+ ) = if (enabled) {
pointerInput(key) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
- if (event.type == PointerEventType.Press) {
- onPress()
- } else if (event.type == PointerEventType.Move) {
- onMove()
- } else if (event.type == PointerEventType.Release) {
- onRelease()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ onEnter()
+ }
+ PointerEventType.Press -> {
+ onPress()
+ }
+ PointerEventType.Move -> {
+ onMove()
+ }
+ PointerEventType.Release -> {
+ onRelease()
+ }
+ PointerEventType.Exit -> {
+ onExit()
+ }
}
}
}
}
} else this
+ private fun Modifier.dynamicPointerInputModifierWithDetectTapGestures(
+ enabled: Boolean,
+ key: Any? = Unit,
+ onTap: () -> Unit = { }
+ ) = if (enabled) {
+ pointerInput(key) {
+ detectTapGestures {
+ onTap()
+ }
+ }
+ } else {
+ this
+ }
+
private fun Modifier.dynamicClickableModifier(
enabled: Boolean,
onClick: () -> Unit
@@ -1365,8 +1394,20 @@
) { onClick() }
} else this
+ // !!!!! MOUSE & TOUCH EVENTS TESTS WITH DYNAMIC MODIFIER INPUT TESTS SECTION (START) !!!!!
+ // The next ~20 tests test enabling/disabling dynamic input modifiers (both pointer input and
+ // clickable) using various combinations (touch vs. mouse, Unit vs. unique keys, nested UI
+ // elements vs. all modifiers on one UI element, etc.)
+
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+ * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
@Test
- fun dynamicClickableModifierTouch_addsAbovePointerInputWithKey_triggersBothModifiers() {
+ fun dynamicClickableModifier_addsAbovePointerInputWithKeyTouchEvents_correctEvents() {
// This is part of a dynamic modifier
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1410,8 +1451,15 @@
}
}
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+ * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
@Test
- fun dynamicClickableModifierTouch_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+ fun dynamicClickableModifier_addsAbovePointerInputWithUnitKeyTouchEvents_correctEvents() {
// This is part of a dynamic modifier
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1455,9 +1503,18 @@
}
}
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+ * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Mouse "click()" (press/release)
+ * 3. Mouse exit
+ * 4. Assert
+ */
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicClickableModifierMouse_addsAbovePointerInputWithKey_triggersBothModifiers() {
+ fun dynamicClickableModifier_addsAbovePointerInputWithKeyMouseEvents_correctEvents() {
// This is part of a dynamic modifier
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1509,9 +1566,18 @@
}
}
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+ * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Mouse "click()" (press/release)
+ * 3. Mouse exit
+ * 4. Assert
+ */
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicClickableModifierMouse_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+ fun dynamicClickableModifier_addsAbovePointerInputWithUnitKeyMouseEvents_correctEvents() {
// This is part of a dynamic modifier
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1563,8 +1629,16 @@
}
}
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
@Test
- fun dynamicInputModifierTouch_addsAboveClickableWithKey_triggersBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableWithKeyTouchEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1603,8 +1677,16 @@
}
}
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
@Test
- fun dynamicInputModifierTouch_addsAboveClickableWithUnitKey_triggersInBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableWithUnitKeyTouchEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1642,9 +1724,22 @@
}
}
- // Tests a dynamic pointer input AND a dynamic clickable{} above an existing pointer input.
+ /* Uses pointer input block for the non-dynamic pointer input and BOTH a clickable{} and
+ * pointer input block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer
+ * inputs (both on same Box).
+ * Both the dynamic Pointer and clickable{} are disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ * 3. Touch down
+ * 4. Assert
+ * 5. Touch move
+ * 6. Assert
+ * 7. Touch up
+ * 8. Assert
+ */
@Test
- fun dynamicInputModifiersInTouchStream_addsAboveClickableWithUnitKey_triggersAllModifiers() {
+ fun dynamicInputAndClickableModifier_addsAbovePointerInputWithUnitKeyTouchEventsWithMove() {
var activeDynamicClickable by mutableStateOf(false)
var dynamicClickableCounter by mutableStateOf(0)
@@ -1676,6 +1771,10 @@
.dynamicClickableModifier(activeDynamicClickable) {
dynamicClickableCounter++
}
+ // Note the .background() above the static pointer input block
+ // TODO (jjw): Remove once bug fixed for when a dynamic pointer input follows
+ // directly after another pointer input (both using Unit key).
+ // Workaround: add a modifier between them OR use unique keys (that is, not Unit)
.background(Color.Green)
.pointerInput(Unit) {
originalPointerInputLambdaExecutionCount++
@@ -1768,13 +1867,19 @@
}
}
- /*
- * Tests adding dynamic modifier with COMPLETE mouse events, that is, the expected events from
- * using a hardware device with an Android device.
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Mouse "click()" (press/release)
+ * 3. Mouse exit
+ * 4. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierMouse_addsAboveClickableWithKey_triggersBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableWithKeyMouseEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1820,13 +1925,19 @@
}
}
- /*
- * Tests adding dynamic modifier with COMPLETE mouse events, that is, the expected events from
- * using a hardware device with an Android device.
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Mouse "click()" (press/release)
+ * 3. Mouse exit
+ * 4. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierMouse_addsAboveClickableWithUnitKey_triggersInBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableWithUnitKeyMouseEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1871,15 +1982,26 @@
}
}
- /* Tests dynamically adding a pointer input DURING an event stream (specifically, Hover).
- * Hover is the only scenario where you can add a new pointer input modifier during the event
- * stream AND receive events in the same active stream from that new pointer input modifier.
- * It isn't possible in the down/up scenario because you add the new modifier during the down
- * but you don't get another down until the next event stream.
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+ *
+ * Tests dynamically adding a pointer input ABOVE an existing pointer input DURING an
+ * event stream (specifically, Hover).
+ *
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Assert
+ * 3. Mouse press
+ * 4. Assert
+ * 5. Mouse release
+ * 6. Assert
+ * 7. Mouse exit
+ * 8. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierHoverMouse_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+ fun dynamicInputModifier_addsAbovePointerInputWithUnitKeyMouseEvents_correctEvents() {
var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
var originalPointerInputEventCounter by mutableStateOf(0)
@@ -1959,14 +2081,17 @@
}
}
- /* This is the same as the test above, but
- * 1. Using clickable{}
- * 2. It enables the dynamic pointer input and starts the hover event stream in a more
- * hacky way (using mouse click without hover which triggers hover enter on release).
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierIncompleteMouse_addsAboveClickableHackyEvents_triggersBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableIncompleteMouseEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1989,16 +2114,6 @@
)
}
- // Usually, a proper event stream from hardware for mouse input would be:
- // - enter() (hover enter)
- // - click()
- // - exit()
- // However, in this case, I'm just calling click() which triggers actions:
- // - press
- // - release
- // - hover enter
- // This starts a hover event stream (in a more hacky way) and also enables the dynamic
- // pointer input to start recording events.
rule.onNodeWithTag("myClickable").performMouseInput {
click()
}
@@ -2018,6 +2133,1749 @@
}
}
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+ *
+ * Tests dynamically adding a pointer input AFTER an existing pointer input DURING an
+ * event stream (specifically, Hover).
+ * Hover is the only scenario where you can add a new pointer input modifier during the event
+ * stream AND receive events in the same active stream from that new pointer input modifier.
+ * It isn't possible in the down/up scenario because you add the new modifier during the down
+ * but you don't get another down until the next event stream.
+ *
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Assert
+ * 3. Mouse press
+ * 4. Assert
+ * 5. Mouse release
+ * 6. Assert
+ * 7. Mouse exit
+ * 8. Assert
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsBelowPointerInputWithUnitKeyMouseEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEventCounter by mutableStateOf(0)
+
+ var dynamicPressCounter by mutableStateOf(0)
+ var dynamicReleaseCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ awaitPointerEvent()
+ originalPointerInputEventCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ }
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onPress = {
+ dynamicPressCounter++
+ },
+ onRelease = {
+ dynamicReleaseCounter++
+ }
+ )
+ )
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEventCounter)
+ assertEquals(0, dynamicPressCounter)
+ assertEquals(0, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ press()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ // Both the original and enabled dynamic pointer input modifiers will get the event
+ // since they are on the same Box.
+ assertEquals(2, originalPointerInputEventCounter)
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(0, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ assertTrue(activateDynamicPointerInput)
+ release()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputEventCounter)
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(1, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(4, originalPointerInputEventCounter)
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(1, dynamicReleaseCounter)
+ }
+ }
+
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEventCounter by mutableStateOf(0)
+
+ var dynamicPressCounter by mutableStateOf(0)
+ var dynamicReleaseCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ awaitPointerEvent()
+ originalPointerInputEventCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ }
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onPress = {
+ dynamicPressCounter++
+ },
+ onRelease = {
+ dynamicReleaseCounter++
+ }
+ )
+ )
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputEventCounter) // Enter, Press, Release
+ assertEquals(0, dynamicPressCounter)
+ assertEquals(0, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ // Because the mouse is still within the box area, Compose doesn't need to trigger an
+ // Exit. Instead, it just triggers two events (Press and Release) which is why the
+ // total is only 5.
+ assertEquals(5, originalPointerInputEventCounter) // Press, Release
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(1, dynamicReleaseCounter)
+ }
+ }
+
+ /* The next set of tests uses two nested boxes inside a box. The two nested boxes each contain
+ * their own pointer input modifier (vs. the tests above that apply two pointer input modifiers
+ * to the same box).
+ */
+
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input.
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_addsBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEventCounter by mutableStateOf(0)
+
+ var dynamicPressCounter by mutableStateOf(0)
+ var dynamicReleaseCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ awaitPointerEvent()
+ originalPointerInputEventCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onPress = {
+ dynamicPressCounter++
+ },
+ onRelease = {
+ dynamicReleaseCounter++
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputEventCounter)
+ assertEquals(0, dynamicPressCounter)
+ assertEquals(0, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputEventCounter)
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(1, dynamicReleaseCounter)
+ }
+ }
+
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input.
+ * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_togglesBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEventCounter by mutableStateOf(0)
+
+ var dynamicPressCounter by mutableStateOf(0)
+ var dynamicReleaseCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ originalPointerInputEventCounter++
+
+ // Note: We only set the activateDynamicPointerInput to true on
+ // Release because we do not want it set on just any event.
+ // Specifically, we do not want it set on Exit, because, in the
+ // case of this event, the exit will be triggered around the same
+ // time as the other dynamic pointer input receives a press (when
+ // it is enabled) because, as soon as that gets that event, Compose
+ // sees this box no longer the hit target (the box with the dynamic
+ // pointer input is now), so it triggers an exit on this original
+ // non-dynamic pointer input. If we allowed
+ // activateDynamicPointerInput to be set during any event, it would
+ // undo us setting activateDynamicPointerInput to false in the other
+ // pointer input handler.
+ if (event.type == PointerEventType.Release) {
+ activateDynamicPointerInput = true
+ }
+ }
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Cyan)
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onPress = {
+ dynamicPressCounter++
+ },
+ onRelease = {
+ dynamicReleaseCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputEventCounter)
+ assertEquals(0, dynamicPressCounter)
+ assertEquals(0, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputEventCounter)
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(1, dynamicReleaseCounter)
+ }
+ }
+
+ /* Uses Foundation's detectTapGestures{} for the non-dynamic pointer input. The dynamic pointer
+ * input uses the lower level pointer input commands.
+ * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowWithUnitKeyIncompleteMouseEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEventCounter by mutableStateOf(0)
+
+ var dynamicPressCounter by mutableStateOf(0)
+ var dynamicReleaseCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ detectTapGestures {
+ originalPointerInputEventCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onPress = {
+ dynamicPressCounter++
+ },
+ onRelease = {
+ dynamicReleaseCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEventCounter)
+ assertEquals(0, dynamicPressCounter)
+ assertEquals(0, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEventCounter)
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(1, dynamicReleaseCounter)
+ }
+ }
+
+ /* Uses Foundation's detectTapGestures{} for both the non-dynamic pointer input and the
+ * dynamic pointer input (vs. the lower level pointer input commands).
+ * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowWithKeyTouchEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("myUniqueKey1") {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = "myUniqueKey2",
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ // This command is the same as
+ // rule.onNodeWithTag("myClickable").performTouchInput { click() }
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertEquals(0, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+
+ /*
+ * The next four tests are based on the test above (nested boxes using a pointer input
+ * modifier blocks with the Foundation Gesture detectTapGestures{}).
+ *
+ * The difference is the dynamic pointer input modifier is enabled to start (while in the
+ * other tests it is disabled to start).
+ *
+ * The tests below tests out variations (mouse vs. touch and Unit keys vs. unique keys).
+ */
+
+ /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIQUE key)
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowOffWithKeyTouchEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(true)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("myUniqueKey1") {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = "myUniqueKey2",
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(0, originalPointerInputLambdaExecutionCount)
+ assertEquals(0, originalPointerInputTapGestureCounter)
+ // Since second box is created following first box, it will get the event
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+
+ /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIT for key)
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowOffWithUnitKeyTouchEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(true)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = Unit,
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(0, originalPointerInputLambdaExecutionCount)
+ assertEquals(0, originalPointerInputTapGestureCounter)
+ // Since second box is created following first box, it will get the event
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+
+ /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIQUE key)
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowOffWithKeyIncompleteMouseEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(true)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("myUniqueKey1") {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = "myUniqueKey2",
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(0, originalPointerInputLambdaExecutionCount)
+ assertEquals(0, originalPointerInputTapGestureCounter)
+ // Since second box is created following first box, it will get the event
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+
+ /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses Unit for key)
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowOffUnitKeyIncompleteMouseEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(true)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = Unit,
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(0, originalPointerInputLambdaExecutionCount)
+ assertEquals(0, originalPointerInputTapGestureCounter)
+ // Since second box is created following first box, it will get the event
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+ // !!!!! MOUSE & TOUCH EVENTS TESTS WITH DYNAMIC MODIFIER INPUT TESTS SECTION (END) !!!!!
+
+ // !!!!! HOVER EVENTS ONLY WITH DYNAMIC MODIFIER INPUT TESTS SECTION (START) !!!!!
+ /* These tests dynamically add a pointer input BEFORE or AFTER an existing pointer input DURING
+ * an event stream (specifically, Hover). Some tests use unique keys while others use UNIT as
+ * the key. Finally, some of the tests apply the modifiers to the same Box while others use
+ * sibling blocks (read the test name for details).
+ *
+ * Test name explains the test.
+ * All tests start with the dynamic pointer disabled and enable it on the first hover enter
+ *
+ * Event sequences:
+ * 1. Hover enter
+ * 2. Assert
+ * 3. Move
+ * 4. Assert
+ * 5. Move
+ * 6. Assert
+ * 7. Hover exit
+ * 8. Assert
+ */
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsAbovePointerInputWithUnitKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+ // same modifier chain), it will receive events as well now.
+ // (Because the dynamic modifier is added BEFORE the original modifier, it WILL reset the
+ // event stream for the original modifier.)
+ // Original pointer input: Hover Exit event then a Hover Enter event
+ // Dynamic pointer input: Hover enter event
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(2, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsAbovePointerInputWithKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .dynamicPointerInputModifier(
+ key = "unique_key_1234",
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ .background(Color.Green)
+ .pointerInput("unique_key_5678") {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+ // same modifier chain), it will receive events as well now.
+ // (Because the dynamic modifier is added BEFORE the original modifier, it WILL reset the
+ // event stream for the original modifier.)
+ // Original pointer input: Hover Exit event then a Hover Enter event
+ // Dynamic pointer input: Hover enter event
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(2, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsBelowPointerInputWithUnitKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ .background(Color.Green)
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+ // same modifier chain), it will receive events as well now.
+ // (Because the dynamic modifier is added BELOW the original modifier, it will not reset the
+ // event stream for the original modifier.)
+ // Original pointer input: Move event
+ // Dynamic pointer input: Hover enter event
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsBelowPointerInputWithKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .pointerInput("unique_key_5678") {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ .background(Color.Green)
+ .dynamicPointerInputModifier(
+ key = "unique_key_1234",
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+ // same modifier chain), it will receive events as well now.
+ // (Because the dynamic modifier is added BELOW the original modifier, it will not reset the
+ // event stream for the original modifier.)
+ // Original pointer input: Move event
+ // Dynamic pointer input: Hover enter event
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_addsBelowPointerInputUnitKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_addsBelowPointerInputKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("unique_key_1234") {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ key = "unique_key_5678",
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_addsAbovePointerInputUnitKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_addsAbovePointerInputKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ key = "unique_key_5678",
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("unique_key_1234") {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+ }
+ // !!!!! HOVER EVENTS ONLY WITH DYNAMIC MODIFIER INPUT TESTS SECTION (END) !!!!!
+
@OptIn(ExperimentalTestApi::class)
@Test
@LargeTest
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuCommon.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuCommon.kt
index a173669..0b3760d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuCommon.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuCommon.kt
@@ -59,7 +59,7 @@
onClick: () -> Unit = {},
) {
item(
- label = label,
+ label = { label },
modifier = modifier,
enabled = enabled,
leadingIcon = leadingIcon,
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingBoundsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingBoundsTest.kt
new file mode 100644
index 0000000..d0e5b20
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingBoundsTest.kt
@@ -0,0 +1,190 @@
+/*
+ * 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.
+ */
+
+package androidx.compose.foundation.text
+
+import android.view.inputmethod.CursorAnchorInfo
+import android.view.inputmethod.ExtractedText
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.safeContentPadding
+import androidx.compose.foundation.setFocusableContent
+import androidx.compose.foundation.text.handwriting.HandwritingBoundsVerticalOffset
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.input.InputMethodInterceptor
+import androidx.compose.foundation.text.input.internal.InputMethodManager
+import androidx.compose.foundation.text.input.internal.inputMethodManagerFactory
+import androidx.compose.foundation.text.matchers.isZero
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CoreTextFieldHandwritingBoundsTest {
+ @get:Rule
+ val rule = createComposeRule()
+ private val inputMethodInterceptor = InputMethodInterceptor(rule)
+
+ private val fakeImm = object : InputMethodManager {
+ private var stylusHandwritingStartCount = 0
+
+ fun expectStylusHandwriting(started: Boolean) {
+ if (started) {
+ assertThat(stylusHandwritingStartCount).isEqualTo(1)
+ stylusHandwritingStartCount = 0
+ } else {
+ assertThat(stylusHandwritingStartCount).isZero()
+ }
+ }
+
+ override fun isActive(): Boolean = true
+
+ override fun restartInput() {}
+
+ override fun showSoftInput() {}
+
+ override fun hideSoftInput() {}
+
+ override fun updateExtractedText(token: Int, extractedText: ExtractedText) {}
+
+ override fun updateSelection(
+ selectionStart: Int,
+ selectionEnd: Int,
+ compositionStart: Int,
+ compositionEnd: Int
+ ) {}
+
+ override fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {}
+
+ override fun startStylusHandwriting() {
+ ++stylusHandwritingStartCount
+ }
+ }
+
+ @Before
+ fun setup() {
+ // Test is only meaningful when stylusHandwriting is supported.
+ assumeTrue(isStylusHandwritingSupported)
+ }
+
+ @Test
+ fun coreTextField_stylusPointerInEditorBounds_focusAndStartHandwriting() {
+ inputMethodManagerFactory = { fakeImm }
+
+ val editorTag1 = "CoreTextField1"
+ val editorTag2 = "CoreTextField2"
+
+ setContent {
+ Column(Modifier.safeContentPadding()) {
+ EditLine(Modifier.testTag(editorTag1))
+ EditLine(Modifier.testTag(editorTag2))
+ }
+ }
+
+ rule.onNodeWithTag(editorTag1).performStylusHandwriting()
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(editorTag1).assertIsFocused()
+ fakeImm.expectStylusHandwriting(true)
+ }
+
+ @Test
+ fun coreTextField_stylusPointerInOverlappingArea_focusedEditorStartHandwriting() {
+ inputMethodManagerFactory = { fakeImm }
+
+ val editorTag1 = "CoreTextField1"
+ val editorTag2 = "CoreTextField2"
+ val spacerTag = "Spacer"
+
+ setContent {
+ Column(Modifier.safeContentPadding()) {
+ EditLine(Modifier.testTag(editorTag1))
+ Spacer(
+ modifier = Modifier.fillMaxWidth()
+ .height(HandwritingBoundsVerticalOffset)
+ .testTag(spacerTag)
+ )
+ EditLine(Modifier.testTag(editorTag2))
+ }
+ }
+
+ rule.onNodeWithTag(editorTag2).requestFocus()
+ rule.waitForIdle()
+
+ // Spacer's height equals to HandwritingBoundsVerticalPadding, both editor will receive the
+ // event.
+ rule.onNodeWithTag(spacerTag).performStylusHandwriting()
+ rule.waitForIdle()
+
+ // Assert that focus didn't change, handwriting is started on the focused editor 2.
+ rule.onNodeWithTag(editorTag2).assertIsFocused()
+ fakeImm.expectStylusHandwriting(true)
+
+ rule.onNodeWithTag(editorTag1).requestFocus()
+ rule.onNodeWithTag(spacerTag).performStylusHandwriting()
+ rule.waitForIdle()
+
+ // Now handwriting is performed on the focused editor 1.
+ rule.onNodeWithTag(editorTag1).assertIsFocused()
+ fakeImm.expectStylusHandwriting(true)
+ }
+
+ @Composable
+ fun EditLine(modifier: Modifier = Modifier) {
+ var value by remember { mutableStateOf(TextFieldValue()) }
+ CoreTextField(
+ value = value,
+ onValueChange = { value = it },
+ modifier = modifier
+ .fillMaxWidth()
+ // make the size of TextFields equal to padding, so that touch bounds of editors
+ // in the same column/row are overlapping.
+ .height(HandwritingBoundsVerticalOffset)
+ )
+ }
+
+ private fun setContent(
+ extraItemForInitialFocus: Boolean = true,
+ content: @Composable () -> Unit
+ ) {
+ rule.setFocusableContent(extraItemForInitialFocus) {
+ inputMethodInterceptor.Content {
+ content()
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
index 966c702..5888282 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
@@ -101,11 +101,6 @@
}
private fun sendTouchEvent(action: Int) {
- val positionInScreen = run {
- val array = intArrayOf(0, 0)
- root.view.getLocationOnScreen(array)
- Offset(array[0].toFloat(), array[1].toFloat())
- }
val motionEvent = MotionEvent.obtain(
/* downTime = */ downTime,
/* eventTime = */ currentTime,
@@ -125,13 +120,13 @@
// test if it handles them properly (versus breaking here and we not knowing
// if Compose properly handles these values).
x = if (startOffset.isValid()) {
- positionInScreen.x + startOffset.x
+ startOffset.x
} else {
Float.NaN
}
y = if (startOffset.isValid()) {
- positionInScreen.y + startOffset.y
+ startOffset.y
} else {
Float.NaN
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/HandwritingDetectorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/HandwritingDetectorTest.kt
index 79686c2..3de2da2 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/HandwritingDetectorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/HandwritingDetectorTest.kt
@@ -16,8 +16,12 @@
package androidx.compose.foundation.text.input
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.safeContentPadding
+import androidx.compose.foundation.text.handwriting.HandwritingBoundsVerticalOffset
import androidx.compose.foundation.text.handwriting.handwritingDetector
import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
import androidx.compose.foundation.text.performStylusClick
@@ -48,7 +52,9 @@
private val imm = FakeInputMethodManager()
- private val tag = "detector"
+ private val detectorTag = "detector"
+ private val insideSpacerTag = "inside"
+ private val outsideSpacerTag = "outside"
private var callbackCount = 0
@@ -62,39 +68,72 @@
callbackCount = 0
rule.setContent {
- Spacer(
- modifier = Modifier
- .fillMaxSize()
- .handwritingDetector { callbackCount++ }
- .testTag(tag)
- )
+ Column(Modifier.safeContentPadding()) {
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(HandwritingBoundsVerticalOffset)
+ .handwritingDetector { callbackCount++ }
+ .testTag(detectorTag)
+ )
+ // This spacer is within the extended handwriting bounds of the detector
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(HandwritingBoundsVerticalOffset)
+ .testTag(insideSpacerTag)
+ )
+ // This spacer is outside the extended handwriting bounds of the detector
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(HandwritingBoundsVerticalOffset)
+ .testTag(outsideSpacerTag)
+ )
+ }
}
}
@Test
fun detector_handwriting_preparesDelegation() {
- rule.onNodeWithTag(tag).performStylusHandwriting()
+ rule.onNodeWithTag(detectorTag).performStylusHandwriting()
assertHandwritingDelegationPrepared()
}
@Test
+ fun detector_handwritingInExtendedBounds_preparesDelegation() {
+ // This spacer is within the extended handwriting bounds of the detector
+ rule.onNodeWithTag(insideSpacerTag).performStylusHandwriting()
+
+ assertHandwritingDelegationPrepared()
+ }
+
+ @Test
+ fun detector_handwritingOutsideExtendedBounds_notPreparesDelegation() {
+ // This spacer is outside the extended handwriting bounds of the detector
+ rule.onNodeWithTag(outsideSpacerTag).performStylusHandwriting()
+
+ assertHandwritingDelegationNotPrepared()
+ }
+
+ @Test
fun detector_click_notPreparesDelegation() {
- rule.onNodeWithTag(tag).performStylusClick()
+ rule.onNodeWithTag(detectorTag).performStylusClick()
assertHandwritingDelegationNotPrepared()
}
@Test
fun detector_longClick_notPreparesDelegation() {
- rule.onNodeWithTag(tag).performStylusLongClick()
+ rule.onNodeWithTag(detectorTag).performStylusLongClick()
assertHandwritingDelegationNotPrepared()
}
@Test
fun detector_longPressAndDrag_notPreparesDelegation() {
- rule.onNodeWithTag(tag).performStylusLongPressAndDrag()
+ rule.onNodeWithTag(detectorTag).performStylusLongPressAndDrag()
assertHandwritingDelegationNotPrepared()
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
index 9495032..cd782ed 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
@@ -56,6 +56,7 @@
import androidx.compose.ui.unit.sp
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.junit.Rule
@@ -392,6 +393,32 @@
assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
+ @Test
+ fun longPress_startingFromEndPadding_draggingUp_selectsFromLastWord_ltr() {
+ val state = TextFieldState("abc def\nghi jkl\nmno pqr")
+ rule.setTextFieldTestContent {
+ BasicTextField(
+ state = state,
+ textStyle = TextStyle(),
+ modifier = Modifier
+ .testTag(TAG)
+ .width(200.dp)
+ )
+ }
+
+ rule.onNodeWithTag(TAG).performTouchInput {
+ longPress(bottomRight)
+ repeat((bottomRight - topRight).y.roundToInt()) {
+ moveBy(Offset(0f, -1f))
+ }
+ up()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.selection).isEqualTo(TextRange(4, 23))
+ }
+ }
+
//region RTL
@Test
@@ -499,6 +526,34 @@
}
@Test
+ fun longPress_startingFromEndPadding_draggingUp_selectsFromLastWord_rtl() {
+ val state = TextFieldState("$rtlText2\n$rtlText2\n$rtlText2")
+ rule.setTextFieldTestContent {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ BasicTextField(
+ state = state,
+ textStyle = TextStyle(),
+ modifier = Modifier
+ .testTag(TAG)
+ .width(200.dp)
+ )
+ }
+ }
+
+ rule.onNodeWithTag(TAG).performTouchInput {
+ longPress(bottomLeft)
+ repeat((bottomLeft - topLeft).y.roundToInt()) {
+ moveBy(Offset(0f, -1f))
+ }
+ up()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.selection).isEqualTo(TextRange(4, 23))
+ }
+ }
+
+ @Test
fun longPress_startDraggingToScrollRight_startHandleDoesNotShow_ltr() {
val state = TextFieldState("abc def ghi ".repeat(10))
rule.setTextFieldTestContent {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt
index d11fe39..56a5472 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt
@@ -254,7 +254,7 @@
* Returns whether or not the context menu should be dismissed.
*/
fun item(
- label: String,
+ label: @Composable () -> String,
modifier: Modifier = Modifier,
enabled: Boolean = true,
/**
@@ -272,11 +272,12 @@
*/
onClick: () -> Unit,
) {
- check(label.isNotBlank()) { "Label must not be blank" }
composables += { colors ->
+ val resolvedLabel = label()
+ check(resolvedLabel.isNotBlank()) { "Label must not be blank" }
ContextMenuItem(
modifier = modifier,
- label = label,
+ label = resolvedLabel,
enabled = enabled,
colors = colors,
leadingIcon = leadingIcon,
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt
index 0425d17..8c17b8a 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text
+import androidx.compose.foundation.contextmenu.ContextMenuScope
import androidx.compose.foundation.contextmenu.ContextMenuState
import androidx.compose.foundation.contextmenu.close
import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
@@ -88,3 +89,15 @@
@Composable
fun resolvedString(): String = stringResource(stringId)
}
+
+internal inline fun ContextMenuScope.TextItem(
+ state: ContextMenuState,
+ label: TextContextMenuItems,
+ enabled: Boolean,
+ crossinline operation: () -> Unit
+) {
+ item(label = { label.resolvedString() }, enabled = enabled) {
+ operation()
+ state.close()
+ }
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt
index 6b69713..ed44b5e 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt
@@ -17,11 +17,11 @@
package androidx.compose.foundation.text.handwriting
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.internal.ComposeInputMethodManager
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.PointerInputModifierNode
@@ -53,7 +53,15 @@
* @sample androidx.compose.foundation.samples.HandwritingDetectorSample
*/
fun Modifier.handwritingDetector(callback: () -> Unit) =
- if (isStylusHandwritingSupported) then(HandwritingDetectorElement(callback)) else this
+ if (isStylusHandwritingSupported) {
+ then(HandwritingDetectorElement(callback))
+ .padding(
+ horizontal = HandwritingBoundsHorizontalOffset,
+ vertical = HandwritingBoundsVerticalOffset
+ )
+ } else {
+ this
+ }
private class HandwritingDetectorElement(
private val callback: () -> Unit
@@ -93,11 +101,9 @@
pointerInputNode.onCancelPointerInput()
}
- val pointerInputNode = delegate(SuspendingPointerInputModifierNode {
- detectStylusHandwriting {
- callback()
- composeImm.prepareStylusHandwritingDelegation()
- return@detectStylusHandwriting true
- }
+ val pointerInputNode = delegate(StylusHandwritingNodeWithNegativePadding {
+ callback()
+ composeImm.prepareStylusHandwritingDelegation()
+ return@StylusHandwritingNodeWithNegativePadding true
})
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.android.kt
index b427583..e770393 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.android.kt
@@ -18,36 +18,16 @@
import androidx.compose.foundation.contextmenu.ContextMenuScope
import androidx.compose.foundation.contextmenu.ContextMenuState
-import androidx.compose.foundation.contextmenu.close
import androidx.compose.foundation.text.TextContextMenuItems
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.foundation.text.TextItem
-@ReadOnlyComposable
-@Composable
internal fun TextFieldSelectionState.contextMenuBuilder(
state: ContextMenuState,
-): ContextMenuScope.() -> Unit {
- val cutString = TextContextMenuItems.Cut.resolvedString()
- val copyString = TextContextMenuItems.Copy.resolvedString()
- val pasteString = TextContextMenuItems.Paste.resolvedString()
- val selectAllString = TextContextMenuItems.SelectAll.resolvedString()
- return {
- item(state, label = cutString, enabled = canCut()) { cut() }
- item(state, label = copyString, enabled = canCopy()) { copy(cancelSelection = false) }
- item(state, label = pasteString, enabled = canPaste()) { paste() }
- item(state, label = selectAllString, enabled = canSelectAll()) { selectAll() }
+): ContextMenuScope.() -> Unit = {
+ TextItem(state, TextContextMenuItems.Cut, enabled = canCut()) { cut() }
+ TextItem(state, TextContextMenuItems.Copy, enabled = canCopy()) {
+ copy(cancelSelection = false)
}
-}
-
-private inline fun ContextMenuScope.item(
- state: ContextMenuState,
- label: String,
- enabled: Boolean,
- crossinline operation: () -> Unit
-) {
- item(label, enabled = enabled) {
- operation()
- state.close()
- }
+ TextItem(state, TextContextMenuItems.Paste, enabled = canPaste()) { paste() }
+ TextItem(state, TextContextMenuItems.SelectAll, enabled = canSelectAll()) { selectAll() }
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
index 4f24fb9..0e4100a6 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
@@ -19,14 +19,12 @@
import androidx.compose.foundation.PlatformMagnifierFactory
import androidx.compose.foundation.contextmenu.ContextMenuScope
import androidx.compose.foundation.contextmenu.ContextMenuState
-import androidx.compose.foundation.contextmenu.close
import androidx.compose.foundation.isPlatformMagnifierSupported
import androidx.compose.foundation.magnifier
import androidx.compose.foundation.text.KeyCommand
import androidx.compose.foundation.text.TextContextMenuItems
+import androidx.compose.foundation.text.TextItem
import androidx.compose.foundation.text.platformDefaultKeyMapping
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -72,31 +70,19 @@
}
}
-@ReadOnlyComposable
-@Composable
internal fun SelectionManager.contextMenuBuilder(
state: ContextMenuState,
-): ContextMenuScope.() -> Unit {
- val copyString = TextContextMenuItems.Copy.resolvedString()
- val selectAllString = TextContextMenuItems.SelectAll.resolvedString()
- return {
- listOf(
- item(
- label = copyString,
- enabled = isNonEmptySelection(),
- onClick = {
- copy()
- state.close()
- },
- ),
- item(
- label = selectAllString,
- enabled = !isEntireContainerSelected(),
- onClick = {
- selectAll()
- state.close()
- },
- ),
- )
- }
+): ContextMenuScope.() -> Unit = {
+ listOf(
+ TextItem(
+ state = state,
+ label = TextContextMenuItems.Copy,
+ enabled = isNonEmptySelection(),
+ ) { copy() },
+ TextItem(
+ state = state,
+ label = TextContextMenuItems.SelectAll,
+ enabled = !isEntireContainerSelected(),
+ ) { selectAll() },
+ )
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
index 3d69358..5aa6139 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
@@ -19,12 +19,10 @@
import androidx.compose.foundation.PlatformMagnifierFactory
import androidx.compose.foundation.contextmenu.ContextMenuScope
import androidx.compose.foundation.contextmenu.ContextMenuState
-import androidx.compose.foundation.contextmenu.close
import androidx.compose.foundation.isPlatformMagnifierSupported
import androidx.compose.foundation.magnifier
import androidx.compose.foundation.text.TextContextMenuItems
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.foundation.text.TextItem
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -70,49 +68,29 @@
}
}
-@ReadOnlyComposable
-@Composable
internal fun TextFieldSelectionManager.contextMenuBuilder(
contextMenuState: ContextMenuState
-): ContextMenuScope.() -> Unit {
- val cutString = TextContextMenuItems.Cut.resolvedString()
- val copyString = TextContextMenuItems.Copy.resolvedString()
- val pasteString = TextContextMenuItems.Paste.resolvedString()
- val selectAllString = TextContextMenuItems.SelectAll.resolvedString()
- return {
- val isPassword = visualTransformation is PasswordVisualTransformation
- val hasSelection = !value.selection.collapsed
- item(
- label = cutString,
- enabled = hasSelection && editable && !isPassword,
- onClick = {
- cut()
- contextMenuState.close()
- },
- )
- item(
- label = copyString,
- enabled = hasSelection && !isPassword,
- onClick = {
- copy(cancelSelection = false)
- contextMenuState.close()
- },
- )
- item(
- label = pasteString,
- enabled = editable && clipboardManager?.hasText() == true,
- onClick = {
- paste()
- contextMenuState.close()
- },
- )
- item(
- label = selectAllString,
- enabled = value.selection.length != value.text.length,
- onClick = {
- selectAll()
- contextMenuState.close()
- },
- )
- }
+): ContextMenuScope.() -> Unit = {
+ val isPassword = visualTransformation is PasswordVisualTransformation
+ val hasSelection = !value.selection.collapsed
+ TextItem(
+ state = contextMenuState,
+ label = TextContextMenuItems.Cut,
+ enabled = hasSelection && editable && !isPassword,
+ ) { cut() }
+ TextItem(
+ state = contextMenuState,
+ label = TextContextMenuItems.Copy,
+ enabled = hasSelection && !isPassword,
+ ) { copy(cancelSelection = false) }
+ TextItem(
+ state = contextMenuState,
+ label = TextContextMenuItems.Paste,
+ enabled = editable && clipboardManager?.hasText() == true,
+ ) { paste() }
+ TextItem(
+ state = contextMenuState,
+ label = TextContextMenuItems.SelectAll,
+ enabled = value.selection.length != value.text.length,
+ ) { selectAll() }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index aaf6abe..44e9b90 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -374,7 +374,8 @@
) : DragGestureNode(
canDrag = AlwaysDrag,
enabled = enabled,
- interactionSource = interactionSource
+ interactionSource = interactionSource,
+ orientationLock = orientation
) {
open suspend fun AnchoredDragScope.anchoredDrag(
@@ -389,9 +390,6 @@
state.anchoredDrag(MutatePriority.Default) { anchoredDrag(forEachDelta) }
}
- override val pointerDirectionConfig: PointerDirectionConfig
- get() = orientation.toPointerDirectionConfig()
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -429,7 +427,8 @@
update(
enabled = enabled,
interactionSource = interactionSource,
- isResetPointerInputHandling = resetPointerInputHandling,
+ shouldResetPointerInputHandling = resetPointerInputHandling,
+ orientationLock = orientation
)
}
@@ -742,7 +741,8 @@
@Deprecated(
message = "Use the progress function to query the progress between two specified " +
"anchors.",
- replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)"))
+ replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)")
+ )
@get:FloatRange(from = 0.0, to = 1.0)
val progress: Float by derivedStateOf(structuralEqualityPolicy()) {
val a = anchors.positionOf(settledValue)
@@ -1296,7 +1296,7 @@
}
}
-private fun<K> ObjectFloatMap<K>.minValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.minValueOrNaN(): Float {
if (size == 1) return Float.NaN
var minValue = Float.POSITIVE_INFINITY
forEachValue { value ->
@@ -1307,7 +1307,7 @@
return minValue
}
-private fun<K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
if (size == 1) return Float.NaN
var maxValue = Float.NEGATIVE_INFINITY
forEachValue { value ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 4d7c7fc..86835e4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -45,7 +45,7 @@
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
-import kotlin.math.abs
+import kotlin.math.absoluteValue
import kotlin.math.sign
import kotlinx.coroutines.CancellationException
@@ -79,7 +79,7 @@
pointerId,
PointerType.Touch,
onPointerSlopReached = onTouchSlopReached,
- pointerDirectionConfig = BidirectionalPointerDirectionConfig,
+ orientation = null,
)
}
@@ -170,33 +170,113 @@
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
+) = detectDragGestures(
+ onDragStart = { _, offset -> onDragStart(offset) },
+ onDragEnd = { onDragEnd.invoke() },
+ onDragCancel = onDragCancel,
+ shouldAwaitTouchSlop = { true },
+ orientationLock = null,
+ onDrag = onDrag
+)
+
+/**
+ * A Gesture detector that waits for pointer down and touch slop in the direction specified by
+ * [orientationLock] and then calls [onDrag] for each drag event.
+ * It follows the touch slop detection of [awaitTouchSlopOrCancellation] but will consume the
+ * position change automatically once the touch slop has been crossed, the amount of drag over
+ * the touch slop is reported as the first drag event [onDrag] after the slop is crossed.
+ * If [shouldAwaitTouchSlop] returns true the touch slop recognition phase will be ignored
+ * and the drag gesture will be recognized immediately.The first [onDrag] in this case will report
+ * an [Offset.Zero].
+ *
+ * [onDragStart] is called when the touch slop has been passed and includes an [Offset] representing
+ * the last known pointer position relative to the containing element as well as the initial
+ * down event that triggered this gesture detection cycle. The [Offset] can be outside
+ * the actual bounds of the element itself meaning the numbers can be negative or larger than the
+ * element bounds if the touch target is smaller than the
+ * [ViewConfiguration.minimumTouchTargetSize].
+ *
+ * [onDragEnd] is called after all pointers are up with the event change of the up event
+ * and [onDragCancel] is called if another gesture has consumed pointer input,
+ * canceling this gesture.
+ *
+ * @param onDragStart A lambda to be called when the drag gesture starts, it contains information
+ * about the triggering [PointerInputChange] and post slop delta.
+ * @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
+ * up [PointerInputChange] that finished the gesture.
+ * @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
+ * when it was consumed.
+ * @param shouldAwaitTouchSlop Indicates if touch slop detection should be skipped.
+ * @param orientationLock Optionally locks detection to this orientation, this means, when this is
+ * provided, touch slop detection and drag event detection will be conditioned to the given
+ * orientation axis. [onDrag] will still dispatch events on with information in both axis, but
+ * if orientation lock is provided, only events that happen on the given orientation will be
+ * considered. If no value is provided (i.e. null) touch slop and drag detection will happen on
+ * an "any" orientation basis, that is, touch slop will be detected if crossed in either direction
+ * and drag events will be dispatched if present in either direction.
+ * @param onDrag A lambda to be called for each delta event in the gesture. It contains information
+ * about the [PointerInputChange] and the movement offset.
+ *
+ * Example Usage:
+ * @sample androidx.compose.foundation.samples.DetectDragGesturesSample
+ *
+ * @see detectVerticalDragGestures
+ * @see detectHorizontalDragGestures
+ * @see detectDragGesturesAfterLongPress to detect gestures after long press
+ */
+internal suspend fun PointerInputScope.detectDragGestures(
+ onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+ onDragEnd: (change: PointerInputChange) -> Unit,
+ onDragCancel: () -> Unit,
+ shouldAwaitTouchSlop: () -> Boolean,
+ orientationLock: Orientation?,
+ onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
awaitEachGesture {
+ val initialDown =
+ awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
+ val awaitTouchSlop = shouldAwaitTouchSlop()
+
+ if (!awaitTouchSlop) {
+ initialDown.consume()
+ }
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
- do {
- drag = awaitPointerSlopOrCancellation(
- down.id,
- down.type,
- pointerDirectionConfig = BidirectionalPointerDirectionConfig
- ) { change, over ->
- change.consume()
- overSlop = over
- }
- } while (drag != null && !drag.isConsumed)
- if (drag != null) {
- onDragStart.invoke(drag.position)
- onDrag(drag, overSlop)
- if (
- !drag(drag.id) {
- onDrag(it, it.positionChange())
- it.consume()
+ var initialDelta = Offset.Zero
+
+ if (awaitTouchSlop) {
+ do {
+ drag = awaitPointerSlopOrCancellation(
+ down.id,
+ down.type,
+ orientation = orientationLock
+ ) { change, over ->
+ change.consume()
+ overSlop = over
}
- ) {
+ } while (drag != null && !drag.isConsumed)
+ initialDelta = drag?.position ?: Offset.Zero
+ } else {
+ drag = initialDown
+ }
+
+ if (drag != null) {
+ onDragStart.invoke(initialDown, initialDelta)
+ onDrag(drag, overSlop)
+ val upEvent = drag(
+ pointerId = drag.id,
+ onDrag = {
+ onDrag(it, it.positionChange())
+ },
+ orientation = orientationLock,
+ motionConsumed = {
+ it.isConsumed
+ })
+ if (upEvent == null) {
onDragCancel()
} else {
- onDragEnd()
+ onDragEnd(upEvent)
}
}
}
@@ -288,7 +368,7 @@
pointerId = pointerId,
pointerType = PointerType.Touch,
onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
- pointerDirectionConfig = VerticalPointerDirectionConfig
+ orientation = Orientation.Vertical
)
internal suspend fun AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(
@@ -299,7 +379,7 @@
pointerId = pointerId,
pointerType = pointerType,
onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
- pointerDirectionConfig = VerticalPointerDirectionConfig
+ orientation = Orientation.Vertical
)
/**
@@ -324,7 +404,7 @@
): Boolean = drag(
pointerId = pointerId,
onDrag = onDrag,
- hasDragged = { it.positionChangeIgnoreConsumed().y != 0f },
+ orientation = Orientation.Vertical,
motionConsumed = { it.isConsumed }
) != null
@@ -439,7 +519,7 @@
pointerId = pointerId,
pointerType = PointerType.Touch,
onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.x) },
- pointerDirectionConfig = HorizontalPointerDirectionConfig
+ orientation = Orientation.Horizontal
)
internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
@@ -450,7 +530,7 @@
pointerId = pointerId,
pointerType = pointerType,
onPointerSlopReached = { change, overSlop -> onPointerSlopReached(change, overSlop.x) },
- pointerDirectionConfig = HorizontalPointerDirectionConfig
+ orientation = Orientation.Horizontal
)
/**
@@ -472,7 +552,7 @@
): Boolean = drag(
pointerId = pointerId,
onDrag = onDrag,
- hasDragged = { it.positionChangeIgnoreConsumed().x != 0f },
+ orientation = Orientation.Horizontal,
motionConsumed = { it.isConsumed }
) != null
@@ -563,9 +643,12 @@
/**
* Continues to read drag events until all pointers are up or the drag event is canceled.
- * The initial pointer to use for driving the drag is [pointerId]. [hasDragged]
- * passes the result whether a change was detected from the drag function or not. [onDrag] is called
- * whenever the pointer moves and [hasDragged] returns non-zero.
+ * The initial pointer to use for driving the drag is [pointerId]. [onDrag] is called
+ * whenever the pointer moves. The up event is returned at the end of the drag gesture.
+ *
+ * @param pointerId The pointer where that is driving the gesture.
+ * @param onDrag Callback for every new drag event.
+ * @param motionConsumed If the PointerInputChange should be considered as consumed.
*
* @return The last pointer input event change when gesture ended with all pointers up
* and null when the gesture was canceled.
@@ -573,7 +656,7 @@
internal suspend inline fun AwaitPointerEventScope.drag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit,
- hasDragged: (PointerInputChange) -> Boolean,
+ orientation: Orientation?,
motionConsumed: (PointerInputChange) -> Boolean
): PointerInputChange? {
if (currentEvent.isPointerUp(pointerId)) {
@@ -581,7 +664,15 @@
}
var pointer = pointerId
while (true) {
- val change = awaitDragOrUp(pointer, hasDragged) ?: return null
+ val change = awaitDragOrUp(pointer) {
+ val positionChange = it.positionChangeIgnoreConsumed()
+ val motionChange = if (orientation == null) {
+ positionChange.getDistance()
+ } else {
+ if (orientation == Orientation.Vertical) positionChange.y else positionChange.x
+ }
+ motionChange != 0.0f
+ } ?: return null
if (motionConsumed(change)) {
return null
@@ -629,16 +720,14 @@
}
/**
- * Waits for drag motion along one axis when [pointerDirectionConfig] is
- * [HorizontalPointerDirectionConfig] or [VerticalPointerDirectionConfig], and drag motion along
- * any axis when using [BidirectionalPointerDirectionConfig]. It passes [pointerId] as the pointer
- * to examine. If [pointerId] is raised, another pointer from those that are down will be chosen to
+ * Waits for drag motion and uses [orientation] to detect the direction of touch slop detection.
+ * It passes [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer from
+ * those that are down will be chosen to
* lead the gesture, and if none are down, `null` is returned. If [pointerId] is not down when
* [awaitPointerSlopOrCancellation] is called, then `null` is returned.
*
* When pointer slop is detected, [onPointerSlopReached] is called with the change and the distance
- * beyond the pointer slop. [PointerDirectionConfig.calculateDeltaChange] should return the position
- * change in the direction of the drag axis. If [onPointerSlopReached] does not consume the
+ * beyond the pointer slop. If [onPointerSlopReached] does not consume the
* position change, pointer slop will not have been considered detected and the detection will
* continue or, if it is consumed, the [PointerInputChange] that was consumed will be returned.
*
@@ -650,10 +739,10 @@
* `null` if all pointers are raised or the position change was consumed by another gesture
* detector.
*/
-internal suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
pointerId: PointerId,
pointerType: PointerType,
- pointerDirectionConfig: PointerDirectionConfig,
+ orientation: Orientation?,
onPointerSlopReached: (PointerInputChange, Offset) -> Unit,
): PointerInputChange? {
if (currentEvent.isPointerUp(pointerId)) {
@@ -661,8 +750,7 @@
}
val touchSlop = viewConfiguration.pointerSlop(pointerType)
var pointer: PointerId = pointerId
- var totalPositionChange = Offset.Zero
-
+ val touchSlopDetector = TouchSlopDetector(orientation)
while (true) {
val event = awaitPointerEvent()
val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
@@ -677,29 +765,8 @@
pointer = otherDown.id
}
} else {
- val currentPosition = dragEvent.position
- val previousPosition = dragEvent.previousPosition
-
- val positionChange = currentPosition - previousPosition
-
- totalPositionChange += positionChange
-
- val inDirection = pointerDirectionConfig.calculateDeltaChange(
- totalPositionChange
- )
-
- if (inDirection < touchSlop) {
- // verify that nothing else consumed the drag event
- awaitPointerEvent(PointerEventPass.Final)
- if (dragEvent.isConsumed) {
- return null
- }
- } else {
- val postSlopOffset = pointerDirectionConfig.calculatePostSlopOffset(
- totalPositionChange,
- touchSlop
- )
-
+ val postSlopOffset = touchSlopDetector.addPointerInputChange(dragEvent, touchSlop)
+ if (postSlopOffset != null) {
onPointerSlopReached(
dragEvent,
postSlopOffset
@@ -707,7 +774,13 @@
if (dragEvent.isConsumed) {
return dragEvent
} else {
- totalPositionChange = Offset.Zero
+ touchSlopDetector.reset()
+ }
+ } else {
+ // verify that nothing else consumed the drag event
+ awaitPointerEvent(PointerEventPass.Final)
+ if (dragEvent.isConsumed) {
+ return null
}
}
}
@@ -715,70 +788,77 @@
}
/**
- * Configures the calculations to get the change amount depending on the dragging type.
- * [calculatePostSlopOffset] will return the post offset slop when the touchSlop is reached.
+ * Detects if touch slop has been crossed after adding a series of [PointerInputChange].
+ * For every new [PointerInputChange] one should add it to this detector using
+ * [addPointerInputChange]. If the position change causes the touch slop to be crossed,
+ * [addPointerInputChange] will return true.
*/
-internal interface PointerDirectionConfig {
- fun calculateDeltaChange(offset: Offset): Float
- fun calculatePostSlopOffset(
- totalPositionChange: Offset,
- touchSlop: Float
- ): Offset
-}
+private class TouchSlopDetector(val orientation: Orientation? = null) {
-/**
- * Used for monitoring changes on X axis.
- */
-internal val HorizontalPointerDirectionConfig = object : PointerDirectionConfig {
- override fun calculateDeltaChange(offset: Offset): Float = abs(offset.x)
+ fun Offset.mainAxis() = if (orientation == Orientation.Horizontal) x else y
+ fun Offset.crossAxis() = if (orientation == Orientation.Horizontal) y else x
- override fun calculatePostSlopOffset(
- totalPositionChange: Offset,
+ /**
+ * The accumulation of drag deltas in this detector.
+ */
+ private var totalPositionChange: Offset = Offset.Zero
+
+ /**
+ * Adds [dragEvent] to this detector. If the accumulated position changes crosses the touch
+ * slop provided by [touchSlop], this method will return the post slop offset, that is the
+ * total accumulated delta change minus the touch slop value, otherwise this should return null.
+ */
+ fun addPointerInputChange(
+ dragEvent: PointerInputChange,
touchSlop: Float
- ): Offset {
- val finalMainPositionChange = totalPositionChange.x -
- (sign(totalPositionChange.x) * touchSlop)
- return Offset(finalMainPositionChange, totalPositionChange.y)
+ ): Offset? {
+ val currentPosition = dragEvent.position
+ val previousPosition = dragEvent.previousPosition
+ val positionChange = currentPosition - previousPosition
+ totalPositionChange += positionChange
+
+ val inDirection = if (orientation == null) {
+ totalPositionChange.getDistance()
+ } else {
+ totalPositionChange.mainAxis().absoluteValue
+ }
+
+ val hasCrossedSlop = inDirection >= touchSlop
+
+ return if (hasCrossedSlop) {
+ calculatePostSlopOffset(touchSlop)
+ } else {
+ null
+ }
+ }
+
+ /**
+ * Resets the accumulator associated with this detector.
+ */
+ fun reset() {
+ totalPositionChange = Offset.Zero
+ }
+
+ private fun calculatePostSlopOffset(touchSlop: Float): Offset {
+ return if (orientation == null) {
+ val touchSlopOffset =
+ totalPositionChange / totalPositionChange.getDistance() * touchSlop
+ // update postSlopOffset
+ totalPositionChange - touchSlopOffset
+ } else {
+ val finalMainAxisChange = totalPositionChange.mainAxis() -
+ (sign(totalPositionChange.mainAxis()) * touchSlop)
+ val finalCrossAxisChange = totalPositionChange.crossAxis()
+ if (orientation == Orientation.Horizontal) {
+ Offset(finalMainAxisChange, finalCrossAxisChange)
+ } else {
+ Offset(finalCrossAxisChange, finalMainAxisChange)
+ }
+ }
}
}
/**
- * Used for monitoring changes on Y axis.
- */
-internal val VerticalPointerDirectionConfig = object : PointerDirectionConfig {
- override fun calculateDeltaChange(offset: Offset): Float = abs(offset.y)
-
- override fun calculatePostSlopOffset(
- totalPositionChange: Offset,
- touchSlop: Float
- ): Offset {
- val finalMainPositionChange = totalPositionChange.y -
- (sign(totalPositionChange.y) * touchSlop)
- return Offset(totalPositionChange.x, finalMainPositionChange)
- }
-}
-
-/**
- * Used for monitoring changes on both X and Y axes.
- */
-internal val BidirectionalPointerDirectionConfig = object : PointerDirectionConfig {
- override fun calculateDeltaChange(offset: Offset): Float = offset.getDistance()
-
- override fun calculatePostSlopOffset(
- totalPositionChange: Offset,
- touchSlop: Float
- ): Offset {
- val touchSlopOffset =
- totalPositionChange / calculateDeltaChange(totalPositionChange) * touchSlop
- return totalPositionChange - touchSlopOffset
- }
-}
-
-internal fun Orientation.toPointerDirectionConfig(): PointerDirectionConfig =
- if (this == Orientation.Vertical) VerticalPointerDirectionConfig
- else HorizontalPointerDirectionConfig
-
-/**
* Waits for a long press by examining [pointerId].
*
* If that [pointerId] is raised (that is, the user lifts their finger), but another
@@ -839,7 +919,7 @@
// should technically never happen as we checked it above
finished = true
}
- // Pointer (id) stayed down.
+ // Pointer (id) stayed down.
} else {
longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 5db29b1..92f3e6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -31,16 +31,11 @@
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
@@ -56,7 +51,6 @@
import kotlin.math.sign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -301,9 +295,10 @@
private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
private var reverseDirection: Boolean
) : DragGestureNode(
- canDrag,
- enabled,
- interactionSource
+ canDrag = canDrag,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = orientation
) {
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
@@ -314,8 +309,6 @@
}
}
- override val pointerDirectionConfig = orientation.toPointerDirectionConfig()
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
this@DraggableNode.onDragStarted(this, startedPosition)
@@ -357,6 +350,7 @@
canDrag,
enabled,
interactionSource,
+ orientation,
resetPointerInputHandling
)
}
@@ -372,6 +366,7 @@
canDrag: (PointerInputChange) -> Boolean,
enabled: Boolean,
interactionSource: MutableInteractionSource?,
+ private var orientationLock: Orientation?
) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
protected var canDrag = canDrag
@@ -397,13 +392,6 @@
abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit)
/**
- * Returns the pointerDirectionConfig which specifies the main and cross axis deltas. This is
- * important when observing the delta change for Draggable, as we want to observe the change
- * in the main axis only.
- */
- abstract val pointerDirectionConfig: PointerDirectionConfig
-
- /**
* Passes the action needed when a drag starts. This gives the ability to pass the desired
* behavior from other nodes implementing AbstractDraggableNode
*/
@@ -478,62 +466,63 @@
// re-create tracker when pointer input block restarts. This lazily creates the tracker
// only when it is need.
val velocityTracker = VelocityTracker()
+ val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
+ { startEvent, initialDelta ->
+ if (canDrag.invoke(startEvent)) {
+ if (!isListeningForEvents) {
+ if (channel == null) {
+ channel = Channel(capacity = Channel.UNLIMITED)
+ }
+ startListeningForEvents()
+ }
+ val overSlopOffset = initialDelta
+ val xSign = sign(startEvent.position.x)
+ val ySign = sign(startEvent.position.y)
+ val adjustedStart = startEvent.position -
+ Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
+
+ channel?.trySend(DragStarted(adjustedStart))
+ }
+ }
+
+ val onDragEnd: (change: PointerInputChange) -> Unit = { upEvent ->
+ velocityTracker.addPointerInputChange(upEvent)
+ val maximumVelocity = currentValueOf(LocalViewConfiguration)
+ .maximumFlingVelocity
+ val velocity = velocityTracker.calculateVelocity(
+ Velocity(maximumVelocity, maximumVelocity)
+ )
+ velocityTracker.resetTracking()
+ channel?.trySend(DragStopped(velocity))
+ }
+
+ val onDragCancel: () -> Unit = {
+ channel?.trySend(DragCancelled)
+ }
+
+ val shouldAwaitTouchSlop: () -> Boolean = {
+ !startDragImmediately()
+ }
+
+ val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
+ { change, delta ->
+ velocityTracker.addPointerInputChange(change)
+ channel?.trySend(DragDelta(delta))
+ }
+
coroutineScope {
try {
- awaitPointerEventScope {
- while (isActive) {
- awaitDownAndSlop(
- _canDrag,
- ::startDragImmediately,
- velocityTracker,
- pointerDirectionConfig
- )?.let {
- /**
- * The gesture crossed the touch slop, events are now relevant
- * and should be propagated
- */
- if (!isListeningForEvents) {
- if (channel == null) {
- channel = Channel(capacity = Channel.UNLIMITED)
- }
- startListeningForEvents()
- }
- var isDragSuccessful = false
- try {
- isDragSuccessful = awaitDrag(
- it.first,
- it.second,
- velocityTracker,
- channel
- ) { event ->
- pointerDirectionConfig.calculateDeltaChange(
- event.positionChangeIgnoreConsumed()
- ) != 0f
- }
- } catch (cancellation: CancellationException) {
- isDragSuccessful = false
- if (!isActive) throw cancellation
- } finally {
- val maximumVelocity = currentValueOf(LocalViewConfiguration)
- .maximumFlingVelocity
- val event = if (isDragSuccessful) {
- val velocity = velocityTracker.calculateVelocity(
- Velocity(maximumVelocity, maximumVelocity)
- )
- velocityTracker.resetTracking()
- DragStopped(velocity)
- } else {
- DragCancelled
- }
- channel?.trySend(event)
- }
- }
- }
- }
- } catch (exception: CancellationException) {
- if (!isActive) {
- throw exception
- }
+ detectDragGestures(
+ orientationLock = orientationLock,
+ onDragStart = onDragStart,
+ onDragEnd = onDragEnd,
+ onDragCancel = onDragCancel,
+ shouldAwaitTouchSlop = shouldAwaitTouchSlop,
+ onDrag = onDrag
+ )
+ } catch (cancellation: CancellationException) {
+ channel?.trySend(DragCancelled)
+ if (!isActive) throw cancellation
}
}
}
@@ -580,9 +569,10 @@
canDrag: (PointerInputChange) -> Boolean = this.canDrag,
enabled: Boolean = this.enabled,
interactionSource: MutableInteractionSource? = this.interactionSource,
- isResetPointerInputHandling: Boolean = false
+ orientationLock: Orientation? = this.orientationLock,
+ shouldResetPointerInputHandling: Boolean = false
) {
- var resetPointerInputHandling = isResetPointerInputHandling
+ var resetPointerInputHandling = shouldResetPointerInputHandling
this.canDrag = canDrag
if (this.enabled != enabled) {
@@ -599,91 +589,17 @@
this.interactionSource = interactionSource
}
+ if (this.orientationLock != orientationLock) {
+ this.orientationLock = orientationLock
+ resetPointerInputHandling = true
+ }
+
if (resetPointerInputHandling) {
pointerInputNode?.resetPointerInputHandler()
}
}
}
-private suspend fun AwaitPointerEventScope.awaitDownAndSlop(
- canDrag: (PointerInputChange) -> Boolean,
- startDragImmediately: () -> Boolean,
- velocityTracker: VelocityTracker,
- pointerDirectionConfig: PointerDirectionConfig
-): Pair<PointerInputChange, Offset>? {
- val initialDown =
- awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
- return if (!canDrag(initialDown)) {
- null
- } else if (startDragImmediately()) {
- initialDown.consume()
- velocityTracker.addPointerInputChange(initialDown)
- // since we start immediately we don't wait for slop and the initial delta is 0
- initialDown to Offset.Zero
- } else {
- val down = awaitFirstDown(requireUnconsumed = false)
- velocityTracker.addPointerInputChange(down)
- var initialDelta = Offset.Zero
- val postPointerSlop = { event: PointerInputChange, offset: Offset ->
- velocityTracker.addPointerInputChange(event)
- event.consume()
- initialDelta = offset
- }
-
- val afterSlopResult = awaitPointerSlopOrCancellation(
- down.id,
- down.type,
- pointerDirectionConfig = pointerDirectionConfig,
- onPointerSlopReached = postPointerSlop
- )
-
- if (afterSlopResult != null) afterSlopResult to initialDelta else null
- }
-}
-
-private suspend fun AwaitPointerEventScope.awaitDrag(
- startEvent: PointerInputChange,
- initialDelta: Offset,
- velocityTracker: VelocityTracker,
- channel: SendChannel<DragEvent>?,
- hasDragged: (PointerInputChange) -> Boolean,
-): Boolean {
-
- val overSlopOffset = initialDelta
- val xSign = sign(startEvent.position.x)
- val ySign = sign(startEvent.position.y)
- val adjustedStart = startEvent.position -
- Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
- channel?.trySend(DragStarted(adjustedStart))
-
- channel?.trySend(DragDelta(initialDelta))
-
- return onDragOrUp(hasDragged, startEvent.id) { event ->
- // Velocity tracker takes all events, even UP
- velocityTracker.addPointerInputChange(event)
-
- // Dispatch only MOVE events
- if (!event.changedToUpIgnoreConsumed()) {
- val delta = event.positionChange()
- event.consume()
- channel?.trySend(DragDelta(delta))
- }
- }
-}
-
-private suspend fun AwaitPointerEventScope.onDragOrUp(
- hasDragged: (PointerInputChange) -> Boolean,
- pointerId: PointerId,
- onDrag: (PointerInputChange) -> Unit
-): Boolean {
- return drag(
- pointerId = pointerId,
- onDrag = onDrag,
- hasDragged = hasDragged,
- motionConsumed = { it.isConsumed }
- )?.let(onDrag) != null
-}
-
private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState {
private val dragScope: DragScope = object : DragScope {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
index f9c9571..0c4b4c4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
@@ -267,9 +267,10 @@
private var onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
private var reverseDirection: Boolean
) : DragGestureNode(
- canDrag,
- enabled,
- interactionSource
+ canDrag = canDrag,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = null
) {
override suspend fun drag(
@@ -282,8 +283,6 @@
}
}
- override val pointerDirectionConfig = BidirectionalPointerDirectionConfig
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
this@Draggable2DNode.onDragStarted(this, startedPosition)
@@ -317,10 +316,11 @@
this.startDragImmediately = startDragImmediately
update(
- canDrag,
- enabled,
- interactionSource,
- resetPointerInputHandling
+ canDrag = canDrag,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = null,
+ shouldResetPointerInputHandling = resetPointerInputHandling
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index cf62744a..bcfaf97 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -273,7 +273,8 @@
) : DragGestureNode(
canDrag = CanDragCalculation,
enabled = enabled,
- interactionSource = interactionSource
+ interactionSource = interactionSource,
+ orientationLock = orientation
), ObserverModifierNode, CompositionLocalConsumerModifierNode,
FocusPropertiesModifierNode, KeyInputModifierNode {
@@ -333,9 +334,6 @@
scrollingLogic.dispatchDragEvents(forEachDelta)
}
- override val pointerDirectionConfig: PointerDirectionConfig
- get() = scrollingLogic.pointerDirectionConfig()
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -386,7 +384,13 @@
this.flingBehavior = flingBehavior
// update DragGestureNode
- update(CanDragCalculation, enabled, interactionSource, resetPointerInputHandling)
+ update(
+ canDrag = CanDragCalculation,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = if (scrollingLogic.isVertical()) Vertical else Horizontal,
+ shouldResetPointerInputHandling = resetPointerInputHandling
+ )
}
override fun onAttach() {
@@ -777,8 +781,6 @@
return resetPointerInputHandling
}
- fun pointerDirectionConfig(): PointerDirectionConfig = orientation.toPointerDirectionConfig()
-
fun isVertical(): Boolean = orientation == Vertical
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 5928a6e..886f376 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -960,10 +960,6 @@
val mainAxisOffset = itemScrollOffsets.maxInRange(spanRange)
val crossAxisOffset = resolvedSlots.positions[laneIndex]
- if (item.placeablesCount == 0) {
- // nothing to place, ignore spacings
- continue
- }
item.position(
mainAxis = mainAxisOffset,
crossAxis = crossAxisOffset,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 50eccc5..8bd9033 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -27,8 +27,7 @@
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
-import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
-import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.handwriting.stylusHandwriting
import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -82,6 +81,7 @@
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
@@ -405,34 +405,6 @@
textDragObserver = manager.touchSelectionObserver,
)
.pointerHoverIcon(textPointerIcon)
- .then(
- if (isStylusHandwritingSupported && writeable) {
- Modifier.pointerInput(Unit) {
- detectStylusHandwriting {
- if (!state.hasFocus) {
- focusRequester.requestFocus()
- }
- // If this is a password field, we can't trigger handwriting.
- // The expected behavior is 1) request focus 2) show software keyboard.
- // Note: TextField will show software keyboard automatically when it
- // gain focus. 3) show a toast message telling that handwriting is not
- // supported for password fields. TODO(b/335294152)
- if (imeOptions.keyboardType != KeyboardType.Password) {
- // TextInputService is calling LegacyTextInputServiceAdapter under the
- // hood. And because it's a public API, startStylusHandwriting is added
- // to legacyTextInputServiceAdapter instead.
- // startStylusHandwriting may be called before the actual input
- // session starts when the editor is not focused, this is handled
- // internally by the LegacyTextInputServiceAdapter.
- legacyTextInputServiceAdapter.startStylusHandwriting()
- }
- true
- }
- }
- } else {
- Modifier
- }
- )
val drawModifier = Modifier.drawBehind {
state.layoutResult?.let { layoutResult ->
@@ -657,10 +629,32 @@
imeAction = imeOptions.imeAction,
)
+ val stylusHandwritingModifier = Modifier.stylusHandwriting(writeable) {
+ if (!state.hasFocus) {
+ focusRequester.requestFocus()
+ }
+ // If this is a password field, we can't trigger handwriting.
+ // The expected behavior is 1) request focus 2) show software keyboard.
+ // Note: TextField will show software keyboard automatically when it
+ // gain focus. 3) show a toast message telling that handwriting is not
+ // supported for password fields. TODO(b/335294152)
+ if (imeOptions.keyboardType != KeyboardType.Password) {
+ // TextInputService is calling LegacyTextInputServiceAdapter under the
+ // hood. And because it's a public API, startStylusHandwriting is added
+ // to legacyTextInputServiceAdapter instead.
+ // startStylusHandwriting may be called before the actual input
+ // session starts when the editor is not focused, this is handled
+ // internally by the LegacyTextInputServiceAdapter.
+ legacyTextInputServiceAdapter.startStylusHandwriting()
+ }
+ true
+ }
+
// Modifiers that should be applied to the outer text field container. Usually those include
// gesture and semantics modifiers.
val decorationBoxModifier = modifier
.legacyTextInputAdapter(legacyTextInputServiceAdapter, state, manager)
+ .then(stylusHandwritingModifier)
.then(focusModifier)
.interceptDPadAndMoveFocus(state, focusManager)
.previewKeyEventToDeselectOnBack(state, manager)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
index 3d4ba3b..4909b8f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
@@ -18,69 +18,194 @@
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.layout.padding
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusEventModifierNode
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
+import androidx.compose.ui.layout.Measurable
+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.PointerInputModifierNode
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
import androidx.compose.ui.util.fastFirstOrNull
/**
- * A utility function that detects stylus movements and calls the [onHandwritingSlopExceeded] when
+ * A modifier that detects stylus movements and calls the [onHandwritingSlopExceeded] when
* it detects that stylus movement has exceeds the handwriting slop.
- * If [onHandwritingSlopExceeded] returns true, this method will consume the events and consider
+ * If [onHandwritingSlopExceeded] returns true, it will consume the events and consider
* that the handwriting has successfully started. Otherwise, it'll stop monitoring the current
* gesture.
+ * @param enabled whether this modifier is enabled, it's used for the case where the editor is
+ * readOnly or disabled.
+ * @param onHandwritingSlopExceeded the callback that's invoked when it detects stylus handwriting.
+ * The return value determines whether the handwriting is triggered or not. When it's true, this
+ * modifier will consume the pointer events.
*/
-internal suspend inline fun PointerInputScope.detectStylusHandwriting(
- crossinline onHandwritingSlopExceeded: () -> Boolean
-) {
- awaitEachGesture {
- val firstDown =
- awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
+internal fun Modifier.stylusHandwriting(
+ enabled: Boolean,
+ onHandwritingSlopExceeded: () -> Boolean
+): Modifier = if (enabled && isStylusHandwritingSupported) {
+ this.then(StylusHandwritingElementWithNegativePadding(onHandwritingSlopExceeded))
+ .padding(
+ horizontal = HandwritingBoundsHorizontalOffset,
+ vertical = HandwritingBoundsVerticalOffset
+ )
+} else {
+ this
+}
- val isStylus =
- firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
- if (!isStylus) {
- return@awaitEachGesture
+private data class StylusHandwritingElementWithNegativePadding(
+ val onHandwritingSlopExceeded: () -> Boolean
+) : ModifierNodeElement<StylusHandwritingNodeWithNegativePadding>() {
+ override fun create(): StylusHandwritingNodeWithNegativePadding {
+ return StylusHandwritingNodeWithNegativePadding(onHandwritingSlopExceeded)
+ }
+
+ override fun update(node: StylusHandwritingNodeWithNegativePadding) {
+ node.onHandwritingSlopExceeded = onHandwritingSlopExceeded
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "stylusHandwriting"
+ properties["onHandwritingSlopExceeded"] = onHandwritingSlopExceeded
+ }
+}
+
+/**
+ * A stylus handwriting node with negative padding. This node should be used in pair with a padding
+ * modifier. Together, they expands the touch bounds of the editor while keep its visual bounds the
+ * same.
+ * Note: this node is a temporary solution, ideally we don't need it.
+ */
+internal class StylusHandwritingNodeWithNegativePadding(
+ onHandwritingSlopExceeded: () -> Boolean
+) : StylusHandwritingNode(onHandwritingSlopExceeded), LayoutModifierNode {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val paddingVerticalPx = HandwritingBoundsVerticalOffset.roundToPx()
+ val paddingHorizontalPx = HandwritingBoundsHorizontalOffset.roundToPx()
+ val newConstraint = constraints.offset(
+ 2 * paddingHorizontalPx,
+ 2 * paddingVerticalPx
+ )
+ val placeable = measurable.measure(newConstraint)
+
+ val height = placeable.height - paddingVerticalPx * 2
+ val width = placeable.width - paddingHorizontalPx * 2
+ return layout(width, height) {
+ placeable.place(-paddingHorizontalPx, -paddingVerticalPx)
}
- // Await the touch slop before long press timeout.
- var exceedsTouchSlop: PointerInputChange? = null
- // The stylus move must exceeds touch slop before long press timeout.
- while (true) {
- val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Main)
- // The tracked pointer is consumed or lifted, stop tracking.
- val change = pointerEvent.changes.fastFirstOrNull {
- !it.isConsumed && it.id == firstDown.id && it.pressed
- }
- if (change == null) {
- break
+ }
+
+ override fun sharePointerInputWithSiblings(): Boolean {
+ // Share events to siblings so that the expanded touch bounds won't block other elements
+ // surrounding the editor.
+ return true
+ }
+}
+
+internal open class StylusHandwritingNode(
+ var onHandwritingSlopExceeded: () -> Boolean
+) : DelegatingNode(), PointerInputModifierNode, FocusEventModifierNode {
+
+ private var focused = false
+
+ override fun onFocusEvent(focusState: FocusState) {
+ focused = focusState.isFocused
+ }
+
+ private val suspendingPointerInputModifierNode = delegate(SuspendingPointerInputModifierNode {
+ awaitEachGesture {
+ val firstDown =
+ awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
+
+ val isStylus =
+ firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
+ if (!isStylus) {
+ return@awaitEachGesture
}
- val time = change.uptimeMillis - firstDown.uptimeMillis
- if (time >= viewConfiguration.longPressTimeoutMillis) {
- break
+ val isInBounds = firstDown.position.x >= 0 && firstDown.position.x < size.width &&
+ firstDown.position.y >= 0 && firstDown.position.y < size.height
+
+ // If the editor is focused or the first down is within the editor's bounds, we
+ // await the initial pass. This prioritize the focused editor over unfocused
+ // editor.
+ val pass = if (focused || isInBounds) {
+ PointerEventPass.Initial
+ } else {
+ PointerEventPass.Main
}
- val offset = change.position - firstDown.position
- if (offset.getDistance() > viewConfiguration.handwritingSlop) {
- exceedsTouchSlop = change
- break
+ // Await the touch slop before long press timeout.
+ var exceedsTouchSlop: PointerInputChange? = null
+ // The stylus move must exceeds touch slop before long press timeout.
+ while (true) {
+ val pointerEvent = awaitPointerEvent(pass)
+ // The tracked pointer is consumed or lifted, stop tracking.
+ val change = pointerEvent.changes.fastFirstOrNull {
+ !it.isConsumed && it.id == firstDown.id && it.pressed
+ }
+ if (change == null) {
+ break
+ }
+
+ val time = change.uptimeMillis - firstDown.uptimeMillis
+ if (time >= viewConfiguration.longPressTimeoutMillis) {
+ break
+ }
+
+ val offset = change.position - firstDown.position
+ if (offset.getDistance() > viewConfiguration.handwritingSlop) {
+ exceedsTouchSlop = change
+ break
+ }
+ }
+
+ if (exceedsTouchSlop == null || !onHandwritingSlopExceeded.invoke()) {
+ return@awaitEachGesture
+ }
+ exceedsTouchSlop.consume()
+
+ // Consume the remaining changes of this pointer.
+ while (true) {
+ val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
+ val pointerChange = pointerEvent.changes.fastFirstOrNull {
+ !it.isConsumed && it.id == firstDown.id && it.pressed
+ } ?: return@awaitEachGesture
+ pointerChange.consume()
}
}
+ })
- if (exceedsTouchSlop == null || !onHandwritingSlopExceeded.invoke()) {
- return@awaitEachGesture
- }
- exceedsTouchSlop.consume()
+ override fun onPointerEvent(
+ pointerEvent: PointerEvent,
+ pass: PointerEventPass,
+ bounds: IntSize
+ ) {
+ suspendingPointerInputModifierNode.onPointerEvent(pointerEvent, pass, bounds)
+ }
- // Consume the remaining changes of this pointer.
- while (true) {
- val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
- val pointerChange = pointerEvent.changes.fastFirstOrNull {
- !it.isConsumed && it.id == firstDown.id && it.pressed
- } ?: return@awaitEachGesture
- pointerChange.consume()
- }
+ override fun onCancelPointerInput() {
+ suspendingPointerInputModifierNode.onCancelPointerInput()
+ }
+
+ fun resetPointerInputHandler() {
+ suspendingPointerInputModifierNode.resetPointerInputHandler()
}
}
@@ -89,3 +214,9 @@
* and NOT for checking whether the IME supports handwriting.
*/
internal expect val isStylusHandwritingSupported: Boolean
+
+/**
+ * The amount of the padding added to the handwriting bounds of an editor.
+ */
+internal val HandwritingBoundsVerticalOffset = 40.dp
+internal val HandwritingBoundsHorizontalOffset = 10.dp
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index eeddf40..5d02a03f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -29,7 +29,7 @@
import androidx.compose.foundation.text.Handle
import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
+import androidx.compose.foundation.text.handwriting.StylusHandwritingNode
import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.KeyboardActionHandler
@@ -225,41 +225,36 @@
detectTextFieldLongPressAndAfterDrag(requestFocus)
}
}
- // Note: when editable changes (enabled or readOnly changes) or keyboard type changes,
- // this pointerInputModifier is reset. And we don't need to worry about cancel or launch
- // the stylus handwriting detecting job.
- if (isStylusHandwritingSupported && editable) {
- launch(start = CoroutineStart.UNDISPATCHED) {
- detectStylusHandwriting {
- if (!isFocused) {
- requestFocus()
- }
- // If this is a password field, we can't trigger handwriting.
- // The expected behavior is 1) request focus 2) show software keyboard.
- // Note: TextField will show software keyboard automatically when it
- // gain focus. 3) show a toast message telling that handwriting is not
- // supported for password fields. TODO(b/335294152)
- if (keyboardOptions.keyboardType != KeyboardType.Password) {
- // Send the handwriting start signal to platform.
- // The editor should send the signal when it is focused or is about
- // to gain focus, Here are more details:
- // 1) if the editor already has an active input session, the
- // platform handwriting service should already listen to this flow
- // and it'll start handwriting right away.
- //
- // 2) if the editor is not focused, but it'll be focused and
- // create a new input session, one handwriting signal will be
- // replayed when the platform collect this flow. And the platform
- // should trigger handwriting accordingly.
- stylusHandwritingTrigger?.tryEmit(Unit)
- }
- return@detectStylusHandwriting true
- }
- }
- }
}
})
+ private val stylusHandwritingNode = delegate(StylusHandwritingNode {
+ if (!isFocused) {
+ requestFocus()
+ }
+
+ // If this is a password field, we can't trigger handwriting.
+ // The expected behavior is 1) request focus 2) show software keyboard.
+ // Note: TextField will show software keyboard automatically when it
+ // gain focus. 3) show a toast message telling that handwriting is not
+ // supported for password fields. TODO(b/335294152)
+ if (keyboardOptions.keyboardType != KeyboardType.Password) {
+ // Send the handwriting start signal to platform.
+ // The editor should send the signal when it is focused or is about
+ // to gain focus, Here are more details:
+ // 1) if the editor already has an active input session, the
+ // platform handwriting service should already listen to this flow
+ // and it'll start handwriting right away.
+ //
+ // 2) if the editor is not focused, but it'll be focused and
+ // create a new input session, one handwriting signal will be
+ // replayed when the platform collect this flow. And the platform
+ // should trigger handwriting accordingly.
+ stylusHandwritingTrigger?.tryEmit(Unit)
+ }
+ return@StylusHandwritingNode true
+ })
+
/**
* The last enter event that was submitted to [interactionSource] from [dragAndDropNode]. We
* need to keep a reference to this event to send a follow-up exit event.
@@ -458,6 +453,7 @@
if (textFieldSelectionState != previousTextFieldSelectionState) {
pointerInputNode.resetPointerInputHandler()
+ stylusHandwritingNode.resetPointerInputHandler()
if (isAttached) {
textFieldSelectionState.receiveContentConfiguration =
receiveContentConfigurationProvider
@@ -466,6 +462,7 @@
if (interactionSource != previousInteractionSource) {
pointerInputNode.resetPointerInputHandler()
+ stylusHandwritingNode.resetPointerInputHandler()
}
}
@@ -604,6 +601,7 @@
disposeInputSession()
textFieldState.collapseSelectionToMax()
}
+ stylusHandwritingNode.onFocusEvent(focusState)
}
override fun onAttach() {
@@ -625,10 +623,12 @@
pass: PointerEventPass,
bounds: IntSize
) {
+ stylusHandwritingNode.onPointerEvent(pointerEvent, pass, bounds)
pointerInputNode.onPointerEvent(pointerEvent, pass, bounds)
}
override fun onCancelPointerInput() {
+ stylusHandwritingNode.onCancelPointerInput()
pointerInputNode.onCancelPointerInput()
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
index 3d55c6a..46181b9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
@@ -770,6 +770,14 @@
allowPreviousSelectionCollapsed = false,
)
+ // When drag starts from the end padding, we eventually need to update the start
+ // point once a selection is initiated. Otherwise, startOffset is always calculated
+ // from dragBeginPosition which can refer to different positions on text if
+ // TextField starts scrolling.
+ if (dragBeginOffsetInText == -1 && !newSelection.collapsed) {
+ dragBeginOffsetInText = newSelection.start
+ }
+
// Although we support reversed selection, reversing the selection after it's
// initiated via long press has a visual glitch that's hard to get rid of. When
// handles (start/end) switch places after the selection reverts, draw happens a
@@ -779,14 +787,6 @@
newSelection = newSelection.reverse()
}
- // When drag starts from the end padding, we eventually need to update the start
- // point once a selection is initiated. Otherwise, startOffset is always calculated
- // from dragBeginPosition which can refer to different positions on text if
- // TextField starts scrolling.
- if (dragBeginOffsetInText == -1 && !newSelection.collapsed) {
- dragBeginOffsetInText = newSelection.start
- }
-
// if the new selection is not equal to previous selection, consider updating the
// acting handle. Otherwise, acting handle should remain the same.
if (newSelection != prevSelection) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
index 8b8b02b..3becb0e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
@@ -37,6 +37,12 @@
/**
* Enables text selection for its direct or indirect children.
*
+ * Use of a lazy layout, such as [LazyRow][androidx.compose.foundation.lazy.LazyRow] or
+ * [LazyColumn][androidx.compose.foundation.lazy.LazyColumn], within a [SelectionContainer]
+ * has undefined behavior on text items that aren't composed. For example, texts that aren't
+ * composed will not be included in copy operations and select all will not expand the
+ * selection to include them.
+ *
* @sample androidx.compose.foundation.samples.SelectionSample
*/
@Composable
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index cccbd34..0eadc27 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -266,6 +266,7 @@
</intent-filter>
<intent-filter>
<action android:name="androidx.compose.integration.macrobenchmark.target.CROSSFADE_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
diff --git a/compose/material/material-icons-extended/build.gradle b/compose/material/material-icons-extended/build.gradle
index 324023b..01a08c0 100644
--- a/compose/material/material-icons-extended/build.gradle
+++ b/compose/material/material-icons-extended/build.gradle
@@ -136,3 +136,15 @@
android {
namespace "androidx.compose.material.icons.extended"
}
+
+afterEvaluate {
+ // Workaround for b/337776938
+ if (!project.hasProperty("android.injected.invoked.from.ide")) {
+ tasks.named("lintAnalyzeDebugAndroidTest").configure {
+ it.dependsOn("generateTestFilesDebugAndroidTest")
+ }
+ tasks.named("generateDebugAndroidTestLintModel").configure {
+ it.dependsOn("generateTestFilesDebugAndroidTest")
+ }
+ }
+}
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt
index 0f50354..0df30f3 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt
@@ -18,6 +18,7 @@
import android.os.Build
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.runtime.CompositionLocalProvider
@@ -95,6 +96,29 @@
}
@Test
+ fun snackbar_emptyContent() {
+ rule.setMaterialContent {
+ Snackbar {
+ // empty content should not crash
+ }
+ }
+ }
+
+ @Test
+ fun snackbar_noTextInContent() {
+ val snackbarHeight = 48.dp
+ val contentSize = 20.dp
+ rule.setMaterialContent {
+ Snackbar {
+ // non-text content should not crash
+ Box(Modifier.testTag("content").size(contentSize))
+ }
+ }
+ rule.onNodeWithTag("content")
+ .assertTopPositionInRootIsEqualTo((snackbarHeight - contentSize) / 2)
+ }
+
+ @Test
fun snackbar_shortTextOnly_defaultSizes() {
val snackbar = rule.setMaterialContentForSizeAssertions(
parentMaxWidth = 300.dp
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
index 29673c2..2e490ec 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
@@ -32,10 +32,12 @@
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.LastBaseline
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirst
+import androidx.compose.ui.util.fastForEach
import kotlin.math.max
/**
@@ -245,25 +247,42 @@
content()
}
}) { measurables, constraints ->
- require(measurables.size == 1) {
- "text for Snackbar expected to have exactly only one child"
+ val textPlaceables = ArrayList<Placeable>(measurables.size)
+ var firstBaseline = AlignmentLine.Unspecified
+ var lastBaseline = AlignmentLine.Unspecified
+ var height = 0
+
+ measurables.fastForEach {
+ val placeable = it.measure(constraints)
+ textPlaceables.add(placeable)
+ if (placeable[FirstBaseline] != AlignmentLine.Unspecified &&
+ (firstBaseline == AlignmentLine.Unspecified ||
+ placeable[FirstBaseline] < firstBaseline)) {
+ firstBaseline = placeable[FirstBaseline]
+ }
+ if (placeable[LastBaseline] != AlignmentLine.Unspecified &&
+ (lastBaseline == AlignmentLine.Unspecified ||
+ placeable[LastBaseline] > lastBaseline)) {
+ lastBaseline = placeable[LastBaseline]
+ }
+ height = max(height, placeable.height)
}
- val textPlaceable = measurables.first().measure(constraints)
- val firstBaseline = textPlaceable[FirstBaseline]
- val lastBaseline = textPlaceable[LastBaseline]
- require(firstBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
- require(lastBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
+
+ val hasText = firstBaseline != AlignmentLine.Unspecified &&
+ lastBaseline != AlignmentLine.Unspecified
val minHeight =
- if (firstBaseline == lastBaseline) {
+ if (firstBaseline == lastBaseline || !hasText) {
SnackbarMinHeightOneLine
} else {
SnackbarMinHeightTwoLines
}
- val containerHeight = max(minHeight.roundToPx(), textPlaceable.height)
+ val containerHeight = max(minHeight.roundToPx(), height)
layout(constraints.maxWidth, containerHeight) {
- val textPlaceY = (containerHeight - textPlaceable.height) / 2
- textPlaceable.placeRelative(0, textPlaceY)
+ textPlaceables.fastForEach {
+ val textPlaceY = (containerHeight - it.height) / 2
+ it.placeRelative(0, textPlaceY)
+ }
}
}
}
@@ -316,10 +335,10 @@
)
val firstTextBaseline = textPlaceable[FirstBaseline]
- require(firstTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
val lastTextBaseline = textPlaceable[LastBaseline]
- require(lastTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
- val isOneLine = firstTextBaseline == lastTextBaseline
+ val hasText = firstTextBaseline != AlignmentLine.Unspecified &&
+ lastTextBaseline != AlignmentLine.Unspecified
+ val isOneLine = firstTextBaseline == lastTextBaseline || !hasText
val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width
val textPlaceY: Int
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
index 9aa622d..571508f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
@@ -29,6 +29,10 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionOnScreen
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
@@ -47,6 +51,7 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -188,50 +193,68 @@
@Test
fun dismissibleNavigationDrawer_testOffset_customWidthLarger_whenOpen() {
- val customWidth = NavigationDrawerWidth + 5.dp
+ val customWidth = NavigationDrawerWidth + 20.dp
+ val density = Density(0.5f)
+ lateinit var coords: LayoutCoordinates
rule.setMaterialContent(lightColorScheme()) {
- val drawerState = rememberDrawerState(DrawerValue.Open)
- DismissibleNavigationDrawer(
- drawerState = drawerState,
- drawerContent = {
- DismissibleDrawerSheet(Modifier.width(customWidth)) {
- Box(
- Modifier
- .fillMaxSize()
- .testTag("content")
- )
- }
- },
- content = {}
- )
+ // Reduce density to ensure wide drawer fits on screen
+ CompositionLocalProvider(LocalDensity provides density) {
+ val drawerState = rememberDrawerState(DrawerValue.Open)
+ DismissibleNavigationDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ DismissibleDrawerSheet(Modifier.width(customWidth)) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .onGloballyPositioned {
+ coords = it
+ }
+ )
+ }
+ },
+ content = {}
+ )
+ }
}
- rule.onNodeWithTag("content")
- .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.runOnIdle {
+ assertThat(coords.positionOnScreen().x).isEqualTo(0f)
+ }
}
@Test
fun dismissibleNavigationDrawer_testOffset_customWidthLarger_whenClosed() {
- val customWidth = NavigationDrawerWidth + 5.dp
+ val customWidth = NavigationDrawerWidth + 20.dp
+ val density = Density(0.5f)
+ lateinit var coords: LayoutCoordinates
rule.setMaterialContent(lightColorScheme()) {
- val drawerState = rememberDrawerState(DrawerValue.Closed)
- DismissibleNavigationDrawer(
- drawerState = drawerState,
- drawerContent = {
- DismissibleDrawerSheet(Modifier.width(customWidth)) {
- Box(
- Modifier
- .fillMaxSize()
- .testTag("content")
- )
- }
- },
- content = {}
- )
+ // Reduce density to ensure wide drawer fits on screen
+ CompositionLocalProvider(LocalDensity provides density) {
+ val drawerState = rememberDrawerState(DrawerValue.Closed)
+ DismissibleNavigationDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ DismissibleDrawerSheet(Modifier.width(customWidth)) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .onGloballyPositioned {
+ coords = it
+ }
+ )
+ }
+ },
+ content = {}
+ )
+ }
}
- rule.onNodeWithTag("content")
- .assertLeftPositionInRootIsEqualTo(-customWidth)
+ rule.runOnIdle {
+ with(density) {
+ assertThat(coords.positionOnScreen().x).isWithin(1f).of(-customWidth.toPx())
+ }
+ }
}
@Test
@@ -416,12 +439,16 @@
// When the drawer state is set to Opened
drawerState.snapTo(DrawerValue.Open)
// Then the drawer should be opened
- assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+ }
// When the drawer state is set to Closed
drawerState.snapTo(DrawerValue.Closed)
// Then the drawer should be closed
- assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+ }
}
@Test
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
index 587382f..97f3407 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
@@ -29,6 +29,10 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionOnScreen
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
@@ -49,6 +53,7 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -167,50 +172,68 @@
@Test
fun navigationDrawer_testOffset_customWidthLarger_whenOpen() {
- val customWidth = NavigationDrawerWidth + 5.dp
+ val customWidth = NavigationDrawerWidth + 20.dp
+ val density = Density(0.5f)
+ lateinit var coords: LayoutCoordinates
rule.setMaterialContent(lightColorScheme()) {
- val drawerState = rememberDrawerState(DrawerValue.Open)
- ModalNavigationDrawer(
- drawerState = drawerState,
- drawerContent = {
- ModalDrawerSheet(Modifier.width(customWidth)) {
- Box(
- Modifier
- .fillMaxSize()
- .testTag("content")
- )
- }
- },
- content = {}
- )
+ // Reduce density to ensure wide drawer fits on screen
+ CompositionLocalProvider(LocalDensity provides density) {
+ val drawerState = rememberDrawerState(DrawerValue.Open)
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ ModalDrawerSheet(Modifier.width(customWidth)) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .onGloballyPositioned {
+ coords = it
+ }
+ )
+ }
+ },
+ content = {}
+ )
+ }
}
- rule.onNodeWithTag("content")
- .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.runOnIdle {
+ assertThat(coords.positionOnScreen().x).isEqualTo(0f)
+ }
}
@Test
fun navigationDrawer_testOffset_customWidthLarger_whenClosed() {
- val customWidth = NavigationDrawerWidth + 5.dp
+ val customWidth = NavigationDrawerWidth + 20.dp
+ val density = Density(0.5f)
+ lateinit var coords: LayoutCoordinates
rule.setMaterialContent(lightColorScheme()) {
- val drawerState = rememberDrawerState(DrawerValue.Closed)
- ModalNavigationDrawer(
- drawerState = drawerState,
- drawerContent = {
- ModalDrawerSheet(Modifier.width(customWidth)) {
- Box(
- Modifier
- .fillMaxSize()
- .testTag("content")
- )
- }
- },
- content = {}
- )
+ // Reduce density to ensure wide drawer fits on screen
+ CompositionLocalProvider(LocalDensity provides density) {
+ val drawerState = rememberDrawerState(DrawerValue.Closed)
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ ModalDrawerSheet(Modifier.width(customWidth)) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .onGloballyPositioned {
+ coords = it
+ }
+ )
+ }
+ },
+ content = {}
+ )
+ }
}
- rule.onNodeWithTag("content")
- .assertLeftPositionInRootIsEqualTo(-customWidth)
+ rule.runOnIdle {
+ with(density) {
+ assertThat(coords.positionOnScreen().x).isWithin(1f).of(-customWidth.toPx())
+ }
+ }
}
@Test
@@ -419,12 +442,16 @@
// When the drawer state is set to Opened
drawerState.snapTo(DrawerValue.Open)
// Then the drawer should be opened
- assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+ }
// When the drawer state is set to Closed
drawerState.snapTo(DrawerValue.Closed)
// Then the drawer should be closed
- assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+ }
}
@Test
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt
index 50e761f..38861c1 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.material3
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.internal.Strings
import androidx.compose.material3.internal.getString
@@ -88,6 +89,29 @@
}
@Test
+ fun snackbar_emptyContent() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Snackbar {
+ // empty content should not crash
+ }
+ }
+ }
+
+ @Test
+ fun snackbar_noTextInContent() {
+ val snackbarHeight = 48.dp
+ val contentSize = 20.dp
+ rule.setMaterialContent(lightColorScheme()) {
+ Snackbar {
+ // non-text content should not crash
+ Box(Modifier.testTag("content").size(contentSize))
+ }
+ }
+ rule.onNodeWithTag("content")
+ .assertTopPositionInRootIsEqualTo((snackbarHeight - contentSize) / 2)
+ }
+
+ @Test
fun snackbar_withDismiss_semantics() {
var clicked = false
val snackbarVisuals =
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
index 644850d5..ed987ae 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
@@ -119,29 +119,21 @@
val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value
CompositionLocalProvider(LocalTextStyle provides textStyle) {
when {
- action == null -> OneRowSnackbar(
+ actionOnNewLine && action != null -> NewLineButtonSnackbar(
text = content,
- action = null,
+ action = action,
dismissAction = dismissAction,
- actionTextStyle,
- actionContentColor,
- dismissActionContentColor
- )
- actionOnNewLine -> NewLineButtonSnackbar(
- content,
- action,
- dismissAction,
- actionTextStyle,
- actionContentColor,
- dismissActionContentColor
+ actionTextStyle = actionTextStyle,
+ actionContentColor = actionContentColor,
+ dismissActionContentColor = dismissActionContentColor,
)
else -> OneRowSnackbar(
text = content,
action = action,
dismissAction = dismissAction,
- actionTextStyle,
- actionContentColor,
- dismissActionContentColor
+ actionTextStyle = actionTextStyle,
+ actionTextColor = actionContentColor,
+ dismissActionColor = dismissActionContentColor,
)
}
}
@@ -353,10 +345,10 @@
)
val firstTextBaseline = textPlaceable[FirstBaseline]
- require(firstTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
val lastTextBaseline = textPlaceable[LastBaseline]
- require(lastTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
- val isOneLine = firstTextBaseline == lastTextBaseline
+ val hasText = firstTextBaseline != AlignmentLine.Unspecified &&
+ lastTextBaseline != AlignmentLine.Unspecified
+ val isOneLine = firstTextBaseline == lastTextBaseline || !hasText
val dismissButtonPlaceX = containerWidth - dismissButtonWidth
val actionButtonPlaceX = dismissButtonPlaceX - actionButtonWidth
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt
index df5f37a..95ea8e8 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt
@@ -82,14 +82,13 @@
override suspend fun <R> withFrameNanos(
onFrame: (Long) -> R
): R = suspendCancellableCoroutine { co ->
- lateinit var awaiter: FrameAwaiter<R>
+ val awaiter = FrameAwaiter(onFrame, co)
val hasNewAwaiters = synchronized(lock) {
val cause = failureCause
if (cause != null) {
co.resumeWithException(cause)
return@suspendCancellableCoroutine
}
- awaiter = FrameAwaiter(onFrame, co)
val hadAwaiters = awaiters.isNotEmpty()
awaiters.add(awaiter)
if (!hadAwaiters) hasAwaitersUnlocked.set(1)
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt
index 28e755c..f2833bd 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt
@@ -20,6 +20,7 @@
import android.media.ImageReader
import android.os.Build
import android.os.Looper
+import android.os.Message
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.collection.ObjectList
@@ -55,7 +56,13 @@
if (!layerList.contains(layer)) {
layerList.add(layer)
if (!handler.hasMessages(0)) {
- handler.sendEmptyMessage(0)
+ // we don't run persistLayers() synchronously in order to do less work as there
+ // might be a lot of new layers created during one frame. however we also want
+ // to execute it as soon as possible to be able to persist the layers before
+ // they discard their content. it is possible that there is some other work
+ // scheduled on the main thread which is going to change what layers are drawn.
+ // we use sendMessageAtFrontOfQueue() in order to be executed before that.
+ handler.sendMessageAtFrontOfQueue(Message.obtain())
}
}
}
@@ -80,7 +87,11 @@
1,
PixelFormat.RGBA_8888,
1
- ).also { imageReader = it }
+ ).apply {
+ // We don't care about the result, but release the buffer back to the queue
+ // for subsequent renders to ensure the RenderThread is free as much as possible
+ setOnImageAvailableListener({ it?.acquireLatestImage()?.close() }, handler)
+ }.also { imageReader = it }
val surface = reader.surface
val canvas = LockHardwareCanvasHelper.lockHardwareCanvas(surface)
// on Robolectric even this canvas is not hardware accelerated and drawing render nodes
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Canvas.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Canvas.kt
index d327689..d9faada 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Canvas.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Canvas.kt
@@ -135,6 +135,7 @@
* Add a rotation (in radians clockwise) to the current transform at the given pivot point.
* The pivot coordinate remains unchanged by the rotation transformation
*
+ * @param radians Rotation transform to apply to the [Canvas]
* @param pivotX The x-coord for the pivot point
* @param pivotY The y-coord for the pivot point
*/
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
index 469f415..9a77328 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
@@ -335,6 +335,7 @@
* 240 is blue
* @param saturation The amount of [hue] represented in the color in the range (0..1),
* where 0 has no color and 1 is fully saturated.
+ * @param alpha Alpha channel to apply to the computed color
* @param value The strength of the color, where 0 is black.
* @param colorSpace The RGB color space used to calculate the Color from the HSV values.
*/
@@ -370,6 +371,7 @@
* where 0 has no color and 1 is fully saturated.
* @param lightness A range of (0..1) where 0 is black, 0.5 is fully colored, and 1 is
* white.
+ * @param alpha Alpha channel to apply to the computed color
* @param colorSpace The RGB color space used to calculate the Color from the HSL values.
*/
fun hsl(
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt
index bb9218b..70223b4 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt
@@ -92,6 +92,7 @@
* is represented as a 4 x 5 matrix
* @param column Column index to query the ColorMatrix value. Range is from 0 to 4 as
* [ColorMatrix] is represented as a 4 x 5 matrix
+ * @param v value to update at the given [row] and [column]
*/
inline operator fun set(row: Int, column: Int, v: Float) {
values[(row * 5) + column] = v
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
index a624bad..bffc12d 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
@@ -367,6 +367,7 @@
*
* @param path1 The first operand (for difference, the minuend)
* @param path2 The second operand (for difference, the subtrahend)
+ * @param operation [PathOperation] to apply to the 2 specified paths
*
* @return True if operation succeeded, false otherwise and this path remains unmodified.
*/
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/RenderEffect.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/RenderEffect.kt
index 30ebfe3..3e2848c 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/RenderEffect.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/RenderEffect.kt
@@ -50,6 +50,7 @@
* [RenderEffect] that will blur the contents of an optional input [RenderEffect]. If no
* input [RenderEffect] is provided, the drawing commands on the [GraphicsLayerScope] this
* [RenderEffect] is configured on will be blurred.
+ * @param renderEffect Optional input [RenderEffect] to be blurred
* @param radiusX Blur radius in the horizontal direction
* @param radiusY Blur radius in the vertical direction
* @param edgeTreatment Strategy used to render pixels outside of bounds of the original input
diff --git a/compose/ui/ui-text/lint-baseline.xml b/compose/ui/ui-text/lint-baseline.xml
index e34eca2..d8802c0 100644
--- a/compose/ui/ui-text/lint-baseline.xml
+++ b/compose/ui/ui-text/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.3.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-beta01)" variant="all" version="8.3.0-beta01">
+<issues format="6" by="lint 8.5.0-alpha03" type="baseline" client="gradle" dependencies="false" name="AGP (8.5.0-alpha03)" variant="all" version="8.5.0-alpha03">
<issue
id="NewApi"
@@ -12,7 +12,7 @@
<issue
id="NewApi"
- message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+ message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
errorLine1=" assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -30,7 +30,7 @@
<issue
id="NewApi"
- message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+ message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
errorLine1=" assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -48,7 +48,7 @@
<issue
id="NewApi"
- message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+ message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
errorLine1=" assertThat(textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
@@ -75,7 +75,7 @@
<issue
id="NewApi"
- message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+ message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
errorLine1=" assertThat(textPaint.blendMode).isEqualTo(BlendMode.DstOver)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
diff --git a/compose/ui/ui-unit/api/current.txt b/compose/ui/ui-unit/api/current.txt
index 8af3326..3e20eda 100644
--- a/compose/ui/ui-unit/api/current.txt
+++ b/compose/ui/ui-unit/api/current.txt
@@ -132,8 +132,8 @@
method public long copy(optional float x, optional float y);
method public float getX();
method public float getY();
- method @androidx.compose.runtime.Stable public inline operator long minus(long other);
- method @androidx.compose.runtime.Stable public inline operator long plus(long other);
+ method @androidx.compose.runtime.Stable public operator long minus(long other);
+ method @androidx.compose.runtime.Stable public operator long plus(long other);
property @androidx.compose.runtime.Stable public final float x;
property @androidx.compose.runtime.Stable public final float y;
field public static final androidx.compose.ui.unit.DpOffset.Companion Companion;
@@ -176,8 +176,8 @@
method @androidx.compose.runtime.Stable public operator long div(int other);
method public float getHeight();
method public float getWidth();
- method @androidx.compose.runtime.Stable public inline operator long minus(long other);
- method @androidx.compose.runtime.Stable public inline operator long plus(long other);
+ method @androidx.compose.runtime.Stable public operator long minus(long other);
+ method @androidx.compose.runtime.Stable public operator long plus(long other);
method @androidx.compose.runtime.Stable public operator long times(float other);
method @androidx.compose.runtime.Stable public operator long times(int other);
property @androidx.compose.runtime.Stable public final float height;
diff --git a/compose/ui/ui-unit/api/restricted_current.txt b/compose/ui/ui-unit/api/restricted_current.txt
index d3e260a..7d41176 100644
--- a/compose/ui/ui-unit/api/restricted_current.txt
+++ b/compose/ui/ui-unit/api/restricted_current.txt
@@ -132,8 +132,8 @@
method public long copy(optional float x, optional float y);
method public float getX();
method public float getY();
- method @androidx.compose.runtime.Stable public inline operator long minus(long other);
- method @androidx.compose.runtime.Stable public inline operator long plus(long other);
+ method @androidx.compose.runtime.Stable public operator long minus(long other);
+ method @androidx.compose.runtime.Stable public operator long plus(long other);
property @androidx.compose.runtime.Stable public final float x;
property @androidx.compose.runtime.Stable public final float y;
field public static final androidx.compose.ui.unit.DpOffset.Companion Companion;
@@ -176,8 +176,8 @@
method @androidx.compose.runtime.Stable public operator long div(int other);
method public float getHeight();
method public float getWidth();
- method @androidx.compose.runtime.Stable public inline operator long minus(long other);
- method @androidx.compose.runtime.Stable public inline operator long plus(long other);
+ method @androidx.compose.runtime.Stable public operator long minus(long other);
+ method @androidx.compose.runtime.Stable public operator long plus(long other);
method @androidx.compose.runtime.Stable public operator long times(float other);
method @androidx.compose.runtime.Stable public operator long times(int other);
property @androidx.compose.runtime.Stable public final float height;
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
index 5c5102f..e2d9797 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
@@ -269,21 +269,29 @@
* Returns a copy of this [DpOffset] instance optionally overriding the
* x or y parameter
*/
- fun copy(x: Dp = this.x, y: Dp = this.y): DpOffset = DpOffset(x, y)
+ fun copy(x: Dp = this.x, y: Dp = this.y): DpOffset = DpOffset(packFloats(x.value, y.value))
/**
* Subtract a [DpOffset] from another one.
*/
@Stable
- inline operator fun minus(other: DpOffset) =
- DpOffset(x - other.x, y - other.y)
+ operator fun minus(other: DpOffset) = DpOffset(
+ packFloats(
+ (x - other.x).value,
+ (y - other.y).value
+ )
+ )
/**
* Add a [DpOffset] to another one.
*/
@Stable
- inline operator fun plus(other: DpOffset) =
- DpOffset(x + other.x, y + other.y)
+ operator fun plus(other: DpOffset) = DpOffset(
+ packFloats(
+ (x + other.x).value,
+ (y + other.y).value
+ )
+ )
@Stable
override fun toString(): String =
@@ -297,14 +305,14 @@
/**
* A [DpOffset] with 0 DP [x] and 0 DP [y] values.
*/
- val Zero = DpOffset(0.dp, 0.dp)
+ val Zero = DpOffset(0x0L)
/**
* Represents an offset whose [x] and [y] are unspecified. This is usually a replacement for
* `null` when a primitive value is desired.
* Access to [x] or [y] on an unspecified offset is not allowed.
*/
- val Unspecified = DpOffset(Dp.Unspecified, Dp.Unspecified)
+ val Unspecified = DpOffset(0x7fc00000_7fc00000L)
}
}
@@ -342,7 +350,12 @@
*/
@Stable
fun lerp(start: DpOffset, stop: DpOffset, fraction: Float): DpOffset =
- DpOffset(lerp(start.x, stop.x, fraction), lerp(start.y, stop.y, fraction))
+ DpOffset(
+ packFloats(
+ lerp(start.x.value, stop.x.value, fraction),
+ lerp(start.y.value, stop.y.value, fraction)
+ )
+ )
/**
* Constructs a [DpSize] from [width] and [height] [Dp] values.
@@ -372,21 +385,33 @@
* Returns a copy of this [DpSize] instance optionally overriding the
* width or height parameter
*/
- fun copy(width: Dp = this.width, height: Dp = this.height): DpSize = DpSize(width, height)
+ fun copy(width: Dp = this.width, height: Dp = this.height): DpSize = DpSize(
+ packFloats(width.value, height.value)
+ )
/**
* Subtract a [DpSize] from another one.
*/
@Stable
- inline operator fun minus(other: DpSize) =
- DpSize(width - other.width, height - other.height)
+ operator fun minus(other: DpSize) =
+ DpSize(
+ packFloats(
+ (width - other.width).value,
+ (height - other.height).value
+ )
+ )
/**
* Add a [DpSize] to another one.
*/
@Stable
- inline operator fun plus(other: DpSize) =
- DpSize(width + other.width, height + other.height)
+ operator fun plus(other: DpSize) =
+ DpSize(
+ packFloats(
+ (width + other.width).value,
+ (height + other.height).value
+ )
+ )
@Stable
inline operator fun component1(): Dp = width
@@ -395,16 +420,36 @@
inline operator fun component2(): Dp = height
@Stable
- operator fun times(other: Int): DpSize = DpSize(width * other, height * other)
+ operator fun times(other: Int): DpSize = DpSize(
+ packFloats(
+ (width * other).value,
+ (height * other).value
+ )
+ )
@Stable
- operator fun times(other: Float): DpSize = DpSize(width * other, height * other)
+ operator fun times(other: Float): DpSize = DpSize(
+ packFloats(
+ (width * other).value,
+ (height * other).value
+ )
+ )
@Stable
- operator fun div(other: Int): DpSize = DpSize(width / other, height / other)
+ operator fun div(other: Int): DpSize = DpSize(
+ packFloats(
+ (width / other).value,
+ (height / other).value
+ )
+ )
@Stable
- operator fun div(other: Float): DpSize = DpSize(width / other, height / other)
+ operator fun div(other: Float): DpSize = DpSize(
+ packFloats(
+ (width / other).value,
+ (height / other).value
+ )
+ )
@Stable
override fun toString(): String =
@@ -418,14 +463,14 @@
/**
* A [DpSize] with 0 DP [width] and 0 DP [height] values.
*/
- val Zero = DpSize(0.dp, 0.dp)
+ val Zero = DpSize(0x0L)
/**
* A size whose [width] and [height] are unspecified. This is usually a replacement for
* `null` when a primitive value is desired.
* Access to [width] or [height] on an unspecified size is not allowed.
*/
- val Unspecified = DpSize(Dp.Unspecified, Dp.Unspecified)
+ val Unspecified = DpSize(0x7fc00000_7fc00000L)
}
}
@@ -456,7 +501,12 @@
*/
@Stable
val DpSize.center: DpOffset
- get() = DpOffset(width / 2f, height / 2f)
+ get() = DpOffset(
+ packFloats(
+ (width / 2f).value,
+ (height / 2f).value
+ )
+ )
@Stable
inline operator fun Int.times(size: DpSize) = size * this
@@ -476,7 +526,12 @@
*/
@Stable
fun lerp(start: DpSize, stop: DpSize, fraction: Float): DpSize =
- DpSize(lerp(start.width, stop.width, fraction), lerp(start.height, stop.height, fraction))
+ DpSize(
+ packFloats(
+ lerp(start.width, stop.width, fraction).value,
+ lerp(start.height, stop.height, fraction).value
+ )
+ )
/**
* A four dimensional bounds using [Dp] for units
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index c63e2a9..7af8753 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -252,236 +252,6 @@
assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
}
- // Inserts a new Node at the top of an existing branch (tests removal of duplicate Nodes too).
- @Test
- fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithOnePointerId_correctResult() {
- val pif1 = PointerInputNodeMock()
- val pif2 = PointerInputNodeMock()
- val pif3 = PointerInputNodeMock()
- val pif4 = PointerInputNodeMock()
- val pifNew1 = PointerInputNodeMock()
-
- val pointerId1 = PointerId(1)
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
- hitPathTracker.addHitPath(pointerId1, listOf(pifNew1, pif1, pif2, pif3, pif4))
-
- val expectedRoot = NodeParent().apply {
- children.add(
- Node(pifNew1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif2).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif3).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif4).apply {
- pointerIds.add(pointerId1)
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
- }
-
- @Test
- fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithTwoPointerIds_correctResult() {
- val pif1 = PointerInputNodeMock()
- val pif2 = PointerInputNodeMock()
- val pif3 = PointerInputNodeMock()
- val pif4 = PointerInputNodeMock()
- val pif5 = PointerInputNodeMock()
- val pif6 = PointerInputNodeMock()
- val pif7 = PointerInputNodeMock()
- val pif8 = PointerInputNodeMock()
-
- val pifNew1 = PointerInputNodeMock()
-
- val pointerId1 = PointerId(1)
- val pointerId2 = PointerId(2)
-
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
- hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
-
- hitPathTracker.addHitPath(pointerId2, listOf(pifNew1, pif5, pif6, pif7, pif8))
-
- val expectedRoot = NodeParent().apply {
- children.add(
- Node(pif1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif2).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif3).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif4).apply {
- pointerIds.add(pointerId1)
- }
- )
- }
- )
- }
- )
- }
- )
-
- children.add(
- Node(pifNew1).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif5).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif6).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif7).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif8).apply {
- pointerIds.add(pointerId2)
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
- }
-
- // Inserts a new Node inside an existing branch (tests removal of duplicate Nodes too).
- @Test
- fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithOnePointerId_correctResult() {
- val pif1 = PointerInputNodeMock()
- val pif2 = PointerInputNodeMock()
- val pif3 = PointerInputNodeMock()
- val pif4 = PointerInputNodeMock()
- val pifNew1 = PointerInputNodeMock()
-
- val pointerId1 = PointerId(1)
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pifNew1, pif2, pif3, pif4))
-
- val expectedRoot = NodeParent().apply {
- children.add(
- Node(pif1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pifNew1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif2).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif3).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif4).apply {
- pointerIds.add(pointerId1)
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
- }
-
- @Test
- fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithTwoPointerIds_correctResult() {
- val pif1 = PointerInputNodeMock()
- val pif2 = PointerInputNodeMock()
- val pif3 = PointerInputNodeMock()
- val pif4 = PointerInputNodeMock()
- val pif5 = PointerInputNodeMock()
- val pif6 = PointerInputNodeMock()
- val pif7 = PointerInputNodeMock()
- val pif8 = PointerInputNodeMock()
-
- val pifNew1 = PointerInputNodeMock()
-
- val pointerId1 = PointerId(1)
- val pointerId2 = PointerId(2)
-
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
- hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
-
- hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pifNew1, pif7, pif8))
-
- val expectedRoot = NodeParent().apply {
- children.add(
- Node(pif1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif2).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif3).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif4).apply {
- pointerIds.add(pointerId1)
- }
- )
- }
- )
- }
- )
- }
- )
-
- children.add(
- Node(pif5).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif6).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pifNew1).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif7).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif8).apply {
- pointerIds.add(pointerId2)
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
- }
-
// Inserts a Node in the bottom of an existing branch (tests removal of duplicate Nodes too).
@Test
fun addHitPath_dynamicNodeAddedBelowPartiallyMatchingTreeWithOnePointerId_correctResult() {
@@ -492,8 +262,16 @@
val pifNew1 = PointerInputNodeMock()
val pointerId1 = PointerId(1)
+ // Modifier.Node(s) hit by the first pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+ // Clear any old hits from previous calls (does not really apply here since it's the first
+ // call)
+ hitPathTracker.removeDetachedPointerInputNodes()
+
+ // Modifier.Node(s) hit by the second pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4, pifNew1))
+ // Clear any old hits from previous calls
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -541,10 +319,18 @@
val pointerId1 = PointerId(1)
val pointerId2 = PointerId(2)
+ // Modifier.Node(s) hit by the first pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
+ // Clear any old hits from previous calls (does not really apply here since it's the first
+ // call)
+ hitPathTracker.removeDetachedPointerInputNodes()
+ // Modifier.Node(s) hit by the second pointer input event
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8, pifNew1))
+ // Clear any old hits from previous calls
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1335,7 +1121,7 @@
@Test
fun removeDetachedPointerInputFilters_noNodes_hitResultJustHasRootAndDoesNotCrash() {
val throwable = catchThrowable {
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
}
assertThat(throwable).isNull()
@@ -1373,7 +1159,7 @@
// Act.
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
// Assert.
@@ -1451,7 +1237,7 @@
hitPathTracker.addHitPath(PointerId(0), listOf(root, middle, leaf))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue()
@@ -1478,7 +1264,7 @@
val pointerId = PointerId(0)
hitPathTracker.addHitPath(pointerId, listOf(root, middle, child))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1511,7 +1297,7 @@
val pointerId = PointerId(0)
hitPathTracker.addHitPath(pointerId, listOf(root, middle, leaf))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1570,7 +1356,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1648,7 +1434,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1727,7 +1513,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1825,7 +1611,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1897,7 +1683,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1971,7 +1757,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2070,7 +1856,7 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent()
@@ -2135,7 +1921,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2204,7 +1990,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2294,7 +2080,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2371,7 +2157,7 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent()
@@ -2444,7 +2230,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2524,7 +2310,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2602,7 +2388,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2654,7 +2440,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2721,7 +2507,7 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2787,7 +2573,7 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/SharePointerInputWithSiblingTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/SharePointerInputWithSiblingTest.kt
new file mode 100644
index 0000000..1fac1b6
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/SharePointerInputWithSiblingTest.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2024 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.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.DrawerValue
+import androidx.compose.material.ModalDrawer
+import androidx.compose.material.rememberDrawerState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SharePointerInputWithSiblingTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun Drawer_drawerContentSharePointerInput_cantClickContent() {
+ var box1Clicked = false
+ var box2Clicked = false
+
+ rule.setContent {
+ val drawerState = rememberDrawerState(DrawerValue.Open)
+
+ ModalDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier
+ .fillMaxSize()
+ .testTag("box1")
+ .testPointerInput(sharePointerInputWithSibling = true) {
+ box1Clicked = true
+ }
+ )
+ },
+ content = {
+ Box(Modifier
+ .fillMaxSize()
+ .testTag("box2")
+ .testPointerInput {
+ box2Clicked = true
+ }
+ )
+ }
+ )
+ }
+
+ rule.onNodeWithTag("box1").performClick()
+ assertThat(box1Clicked).isTrue()
+ assertThat(box2Clicked).isFalse()
+ }
+
+ @Test
+ fun stackedBox_doSharePointer() {
+ var box1Clicked = false
+ var box2Clicked = false
+
+ rule.setContent {
+ Box(Modifier.size(50.dp)) {
+ Box(Modifier
+ .fillMaxSize()
+ .testTag("box1")
+ .testPointerInput {
+ box1Clicked = true
+ }
+ )
+
+ Box(Modifier
+ .fillMaxSize()
+ .testTag("box2")
+ .testPointerInput(sharePointerInputWithSibling = true) {
+ box2Clicked = true
+ }
+ )
+ }
+ }
+
+ rule.onNodeWithTag("box2").performClick()
+ assertThat(box1Clicked).isTrue()
+ assertThat(box2Clicked).isTrue()
+ }
+
+ @Test
+ fun stackedBox_parentDisallowShare_doSharePointerWithSibling() {
+ var box1Clicked = false
+ var box2Clicked = false
+
+ rule.setContent {
+ Box(Modifier
+ .size(50.dp)
+ .testPointerInput(sharePointerInputWithSibling = false)
+ ) {
+ Box(Modifier
+ .fillMaxSize()
+ .testTag("box1")
+ .testPointerInput {
+ box1Clicked = true
+ }
+ )
+
+ Box(Modifier
+ .fillMaxSize()
+ .testTag("box2")
+ .testPointerInput(sharePointerInputWithSibling = true) {
+ box2Clicked = true
+ }
+ )
+ }
+ }
+
+ rule.onNodeWithTag("box2").performClick()
+ assertThat(box1Clicked).isTrue()
+ assertThat(box2Clicked).isTrue()
+ }
+
+ @Test
+ fun stackedBox_doSharePointerWithCousin() {
+ var box1Clicked = false
+ var box2Clicked = false
+
+ rule.setContent {
+ Box(Modifier.size(50.dp)) {
+ Box(Modifier
+ .fillMaxSize()
+ .testTag("box1")
+ .testPointerInput {
+ box1Clicked = true
+ }
+ )
+
+ Box(Modifier.fillMaxSize()) {
+ Box(Modifier
+ .fillMaxSize()
+ .testTag("box2")
+ .testPointerInput(sharePointerInputWithSibling = true) {
+ box2Clicked = true
+ }
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("box2").performClick()
+ assertThat(box1Clicked).isTrue()
+ assertThat(box2Clicked).isTrue()
+ }
+
+ @Test
+ fun stackedBox_parentDisallowShare_notSharePointerWithCousin() {
+ var box1Clicked = false
+ var box2Clicked = false
+
+ rule.setContent {
+ Box(Modifier.size(50.dp)) {
+ Box(Modifier.fillMaxSize()
+ .testTag("box1")
+ .testPointerInput {
+ box1Clicked = true
+ }
+ )
+
+ Box(Modifier.fillMaxSize()
+ .testPointerInput(sharePointerInputWithSibling = false)
+ ) {
+ Box(Modifier.fillMaxSize()
+ .testTag("box2")
+ .testPointerInput(sharePointerInputWithSibling = true) {
+ box2Clicked = true
+ }
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("box2").performClick()
+ assertThat(box1Clicked).isFalse()
+ assertThat(box2Clicked).isTrue()
+ }
+
+ @Test
+ fun stackedBox_doSharePointer_untilFirstBoxDisallowShare() {
+ var box1Clicked = false
+ var box2Clicked = false
+ var box3Clicked = false
+
+ rule.setContent {
+ Box(Modifier.size(50.dp)) {
+ Box(Modifier.fillMaxSize()
+ .testTag("box1")
+ .testPointerInput {
+ box1Clicked = true
+ }
+ )
+
+ Box(Modifier.fillMaxSize()
+ .testTag("box2")
+ .testPointerInput(sharePointerInputWithSibling = false) {
+ box2Clicked = true
+ }
+ )
+
+ Box(Modifier.fillMaxSize()
+ .testTag("box3")
+ .testPointerInput(sharePointerInputWithSibling = true) {
+ box3Clicked = true
+ }
+ )
+ }
+ }
+
+ rule.onNodeWithTag("box3").performClick()
+ assertThat(box1Clicked).isFalse()
+ assertThat(box2Clicked).isTrue()
+ assertThat(box3Clicked).isTrue()
+ }
+}
+
+private fun Modifier.testPointerInput(
+ sharePointerInputWithSibling: Boolean = false,
+ onPointerEvent: () -> Unit = {}
+): Modifier = this.then(TestPointerInputElement(sharePointerInputWithSibling, onPointerEvent))
+
+private data class TestPointerInputElement(
+ val sharePointerInputWithSibling: Boolean,
+ val onPointerEvent: () -> Unit
+) : ModifierNodeElement<TestPointerInputNode>() {
+ override fun create(): TestPointerInputNode {
+ return TestPointerInputNode(sharePointerInputWithSibling, onPointerEvent)
+ }
+
+ override fun update(node: TestPointerInputNode) {
+ node.sharePointerInputWithSibling = sharePointerInputWithSibling
+ node.onPointerEvent = onPointerEvent
+ }
+}
+
+private class TestPointerInputNode(
+ var sharePointerInputWithSibling: Boolean,
+ var onPointerEvent: () -> Unit
+) : Modifier.Node(), PointerInputModifierNode {
+ override fun onPointerEvent(
+ pointerEvent: PointerEvent,
+ pass: PointerEventPass,
+ bounds: IntSize
+ ) {
+ onPointerEvent.invoke()
+ }
+
+ override fun onCancelPointerInput() { }
+
+ override fun sharePointerInputWithSiblings(): Boolean {
+ return sharePointerInputWithSibling
+ }
+}
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 3631b76..d4a106f 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
@@ -26,6 +26,7 @@
import android.os.Build.VERSION_CODES.M
import android.os.Build.VERSION_CODES.N
import android.os.Build.VERSION_CODES.O
+import android.os.Build.VERSION_CODES.P
import android.os.Build.VERSION_CODES.Q
import android.os.Build.VERSION_CODES.S
import android.os.Looper
@@ -1426,20 +1427,22 @@
return layer
}
+ // enable new layers on versions supporting render nodes
+ if (isHardwareAccelerated && SDK_INT >= M && SDK_INT != P) {
+ return GraphicsLayerOwnerLayer(
+ graphicsLayer = graphicsContext.createGraphicsLayer(),
+ context = graphicsContext,
+ ownerView = this,
+ drawBlock = drawBlock,
+ invalidateParentLayer = invalidateParentLayer
+ )
+ }
+
// RenderNode is supported on Q+ for certain, but may also be supported on M-O.
// We can't be confident that RenderNode is supported, so we try and fail over to
// the ViewLayer implementation. We'll try even on on P devices, but it will fail
// until ART allows things on the unsupported list on P.
if (isHardwareAccelerated && SDK_INT >= M && isRenderNodeCompatible) {
- if (SDK_INT >= Q) {
- return GraphicsLayerOwnerLayer(
- graphicsLayer = graphicsContext.createGraphicsLayer(),
- context = graphicsContext,
- ownerView = this,
- drawBlock = drawBlock,
- invalidateParentLayer = invalidateParentLayer
- )
- }
try {
return RenderNodeLayer(
this,
diff --git a/compose/ui/ui/src/androidMain/res/values-af/strings.xml b/compose/ui/ui/src/androidMain/res/values-af/strings.xml
index d4ff363..374463d 100644
--- a/compose/ui/ui/src/androidMain/res/values-af/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-af/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Maak navigasiekieslys toe"</string>
<string name="close_sheet" msgid="7573152094250666567">"Maak sigblad toe"</string>
<string name="default_error_message" msgid="8038256446254964252">"Ongeldige invoer"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Opspringvenster"</string>
<string name="range_start" msgid="7097486360902471446">"Begingrens"</string>
<string name="range_end" msgid="5941395253238309765">"Eindgrens"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-am/strings.xml b/compose/ui/ui/src/androidMain/res/values-am/strings.xml
index 63202e8..1a4e3dd 100644
--- a/compose/ui/ui/src/androidMain/res/values-am/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-am/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"የዳሰሳ ምናሌን ዝጋ"</string>
<string name="close_sheet" msgid="7573152094250666567">"ሉህን ዝጋ"</string>
<string name="default_error_message" msgid="8038256446254964252">"ልክ ያልሆነ ግቤት"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"ብቅ-ባይ መስኮት"</string>
<string name="range_start" msgid="7097486360902471446">"የክልል መጀመሪያ"</string>
<string name="range_end" msgid="5941395253238309765">"የክልል መጨረሻ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ar/strings.xml b/compose/ui/ui/src/androidMain/res/values-ar/strings.xml
index 3cc8b9b..58c7262 100644
--- a/compose/ui/ui/src/androidMain/res/values-ar/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ar/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"إغلاق قائمة التنقل"</string>
<string name="close_sheet" msgid="7573152094250666567">"إغلاق الورقة"</string>
<string name="default_error_message" msgid="8038256446254964252">"إدخال غير صالح"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"نافذة منبثقة"</string>
<string name="range_start" msgid="7097486360902471446">"بداية النطاق"</string>
<string name="range_end" msgid="5941395253238309765">"نهاية النطاق"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-as/strings.xml b/compose/ui/ui/src/androidMain/res/values-as/strings.xml
index c68278cc..b6a3786 100644
--- a/compose/ui/ui/src/androidMain/res/values-as/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-as/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"নেভিগেশ্বন মেনু বন্ধ কৰক"</string>
<string name="close_sheet" msgid="7573152094250666567">"শ্বীট বন্ধ কৰক"</string>
<string name="default_error_message" msgid="8038256446254964252">"অমান্য ইনপুট"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"পপ-আপ ৱিণ্ড’"</string>
<string name="range_start" msgid="7097486360902471446">"পৰিসৰৰ আৰম্ভণি"</string>
<string name="range_end" msgid="5941395253238309765">"পৰিসৰৰ সমাপ্তি"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-az/strings.xml b/compose/ui/ui/src/androidMain/res/values-az/strings.xml
index 929a2a6..ccac763 100644
--- a/compose/ui/ui/src/androidMain/res/values-az/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-az/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Naviqasiya menyusunu bağlayın"</string>
<string name="close_sheet" msgid="7573152094250666567">"Səhifəni bağlayın"</string>
<string name="default_error_message" msgid="8038256446254964252">"Yanlış daxiletmə"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Popap Pəncərəsi"</string>
<string name="range_start" msgid="7097486360902471446">"Sıranın başlanğıcı"</string>
<string name="range_end" msgid="5941395253238309765">"Sıranın sonu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-b+sr+Latn/strings.xml b/compose/ui/ui/src/androidMain/res/values-b+sr+Latn/strings.xml
index 5f78ccf..794d4ee 100644
--- a/compose/ui/ui/src/androidMain/res/values-b+sr+Latn/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-b+sr+Latn/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Zatvori meni za navigaciju"</string>
<string name="close_sheet" msgid="7573152094250666567">"Zatvorite tabelu"</string>
<string name="default_error_message" msgid="8038256446254964252">"Unos je nevažeći"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Iskačući prozor"</string>
<string name="range_start" msgid="7097486360902471446">"Početak opsega"</string>
<string name="range_end" msgid="5941395253238309765">"Kraj opsega"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-be/strings.xml b/compose/ui/ui/src/androidMain/res/values-be/strings.xml
index f36b4bb..3be7010 100644
--- a/compose/ui/ui/src/androidMain/res/values-be/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-be/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Закрыць меню навігацыі"</string>
<string name="close_sheet" msgid="7573152094250666567">"Закрыць аркуш"</string>
<string name="default_error_message" msgid="8038256446254964252">"Памылка ўводу"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Усплывальнае акно"</string>
<string name="range_start" msgid="7097486360902471446">"Пачатак пераліку"</string>
<string name="range_end" msgid="5941395253238309765">"Канец пераліку"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-bg/strings.xml b/compose/ui/ui/src/androidMain/res/values-bg/strings.xml
index 68b6856..6814429 100644
--- a/compose/ui/ui/src/androidMain/res/values-bg/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-bg/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Затваряне на менюто за навигация"</string>
<string name="close_sheet" msgid="7573152094250666567">"Затваряне на таблицата"</string>
<string name="default_error_message" msgid="8038256446254964252">"Въведеното е невалидно"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Изскачащ прозорец"</string>
<string name="range_start" msgid="7097486360902471446">"Начало на обхвата"</string>
<string name="range_end" msgid="5941395253238309765">"Край на обхвата"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-bn/strings.xml b/compose/ui/ui/src/androidMain/res/values-bn/strings.xml
index ea9fe9b..2f84058 100644
--- a/compose/ui/ui/src/androidMain/res/values-bn/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-bn/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"নেভিগেশন মেনু বন্ধ করুন"</string>
<string name="close_sheet" msgid="7573152094250666567">"শিট বন্ধ করুন"</string>
<string name="default_error_message" msgid="8038256446254964252">"ভুল ইনপুট"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"পপ-আপ উইন্ডো"</string>
<string name="range_start" msgid="7097486360902471446">"রেঞ্জ শুরু"</string>
<string name="range_end" msgid="5941395253238309765">"রেঞ্জ শেষ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-bs/strings.xml b/compose/ui/ui/src/androidMain/res/values-bs/strings.xml
index acf3667..d8d8334 100644
--- a/compose/ui/ui/src/androidMain/res/values-bs/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-bs/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Zatvaranje navigacionog menija"</string>
<string name="close_sheet" msgid="7573152094250666567">"Zatvaranje tabele"</string>
<string name="default_error_message" msgid="8038256446254964252">"Pogrešan unos"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Skočni prozor"</string>
<string name="range_start" msgid="7097486360902471446">"Početak raspona"</string>
<string name="range_end" msgid="5941395253238309765">"Kraj raspona"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ca/strings.xml b/compose/ui/ui/src/androidMain/res/values-ca/strings.xml
index f85f039..dd44ae7 100644
--- a/compose/ui/ui/src/androidMain/res/values-ca/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ca/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Tanca el menú de navegació"</string>
<string name="close_sheet" msgid="7573152094250666567">"Tanca el full"</string>
<string name="default_error_message" msgid="8038256446254964252">"L\'entrada no és vàlida"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Finestra emergent"</string>
<string name="range_start" msgid="7097486360902471446">"Inici de l\'interval"</string>
<string name="range_end" msgid="5941395253238309765">"Fi de l\'interval"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-cs/strings.xml b/compose/ui/ui/src/androidMain/res/values-cs/strings.xml
index a0c4662..e182a1a 100644
--- a/compose/ui/ui/src/androidMain/res/values-cs/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-cs/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Zavřít navigační panel"</string>
<string name="close_sheet" msgid="7573152094250666567">"Zavřít sešit"</string>
<string name="default_error_message" msgid="8038256446254964252">"Neplatný údaj"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Vyskakovací okno"</string>
<string name="range_start" msgid="7097486360902471446">"Začátek rozsahu"</string>
<string name="range_end" msgid="5941395253238309765">"Konec rozsahu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-da/strings.xml b/compose/ui/ui/src/androidMain/res/values-da/strings.xml
index e440106..792e409 100644
--- a/compose/ui/ui/src/androidMain/res/values-da/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-da/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Luk navigationsmenuen"</string>
<string name="close_sheet" msgid="7573152094250666567">"Luk arket"</string>
<string name="default_error_message" msgid="8038256446254964252">"Ugyldigt input"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Pop op-vindue"</string>
<string name="range_start" msgid="7097486360902471446">"Startinterval"</string>
<string name="range_end" msgid="5941395253238309765">"Slutinterval"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-de/strings.xml b/compose/ui/ui/src/androidMain/res/values-de/strings.xml
index fce4e02..0022a48 100644
--- a/compose/ui/ui/src/androidMain/res/values-de/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-de/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Navigationsmenü schließen"</string>
<string name="close_sheet" msgid="7573152094250666567">"Tabelle schließen"</string>
<string name="default_error_message" msgid="8038256446254964252">"Ungültige Eingabe"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up-Fenster"</string>
<string name="range_start" msgid="7097486360902471446">"Bereichsstart"</string>
<string name="range_end" msgid="5941395253238309765">"Bereichsende"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-el/strings.xml b/compose/ui/ui/src/androidMain/res/values-el/strings.xml
index 6a0e6ee..31496f4 100644
--- a/compose/ui/ui/src/androidMain/res/values-el/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-el/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Κλείσιμο του μενού πλοήγησης"</string>
<string name="close_sheet" msgid="7573152094250666567">"Κλείσιμο φύλλου"</string>
<string name="default_error_message" msgid="8038256446254964252">"Μη έγκυρη καταχώριση"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Αναδυόμενο παράθυρο"</string>
<string name="range_start" msgid="7097486360902471446">"Αρχή εύρους"</string>
<string name="range_end" msgid="5941395253238309765">"Τέλος εύρους"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rAU/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rAU/strings.xml
index 4cd1620..6cec5e5 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rAU/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rAU/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Close navigation menu"</string>
<string name="close_sheet" msgid="7573152094250666567">"Close sheet"</string>
<string name="default_error_message" msgid="8038256446254964252">"Invalid input"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up window"</string>
<string name="range_start" msgid="7097486360902471446">"Range start"</string>
<string name="range_end" msgid="5941395253238309765">"Range end"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rCA/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rCA/strings.xml
index 263002e13..af69c1f 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rCA/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rCA/strings.xml
@@ -31,6 +31,7 @@
<string name="close_drawer" msgid="406453423630273620">"Close navigation menu"</string>
<string name="close_sheet" msgid="7573152094250666567">"Close sheet"</string>
<string name="default_error_message" msgid="8038256446254964252">"Invalid input"</string>
+ <string name="state_empty" msgid="4139871816613051306">"Empty"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"Pop-Up Window"</string>
<string name="range_start" msgid="7097486360902471446">"Range start"</string>
<string name="range_end" msgid="5941395253238309765">"Range end"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rGB/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rGB/strings.xml
index 4cd1620..6cec5e5 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rGB/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rGB/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Close navigation menu"</string>
<string name="close_sheet" msgid="7573152094250666567">"Close sheet"</string>
<string name="default_error_message" msgid="8038256446254964252">"Invalid input"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up window"</string>
<string name="range_start" msgid="7097486360902471446">"Range start"</string>
<string name="range_end" msgid="5941395253238309765">"Range end"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rIN/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rIN/strings.xml
index 4cd1620..6cec5e5 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rIN/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rIN/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Close navigation menu"</string>
<string name="close_sheet" msgid="7573152094250666567">"Close sheet"</string>
<string name="default_error_message" msgid="8038256446254964252">"Invalid input"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up window"</string>
<string name="range_start" msgid="7097486360902471446">"Range start"</string>
<string name="range_end" msgid="5941395253238309765">"Range end"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rXC/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rXC/strings.xml
index 4779808..e19716e 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rXC/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rXC/strings.xml
@@ -31,6 +31,7 @@
<string name="close_drawer" msgid="406453423630273620">"Close navigation menu"</string>
<string name="close_sheet" msgid="7573152094250666567">"Close sheet"</string>
<string name="default_error_message" msgid="8038256446254964252">"Invalid input"</string>
+ <string name="state_empty" msgid="4139871816613051306">"Empty"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"Pop-Up Window"</string>
<string name="range_start" msgid="7097486360902471446">"Range start"</string>
<string name="range_end" msgid="5941395253238309765">"Range end"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-es-rUS/strings.xml b/compose/ui/ui/src/androidMain/res/values-es-rUS/strings.xml
index bf5e07e..db50050 100644
--- a/compose/ui/ui/src/androidMain/res/values-es-rUS/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-es-rUS/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Cerrar el menú de navegación"</string>
<string name="close_sheet" msgid="7573152094250666567">"Cerrar hoja"</string>
<string name="default_error_message" msgid="8038256446254964252">"Entrada no válida"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Ventana emergente"</string>
<string name="range_start" msgid="7097486360902471446">"Inicio de intervalo"</string>
<string name="range_end" msgid="5941395253238309765">"Final de intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-es/strings.xml b/compose/ui/ui/src/androidMain/res/values-es/strings.xml
index 68c06e1..3b25bdd 100644
--- a/compose/ui/ui/src/androidMain/res/values-es/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-es/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Cerrar menú de navegación"</string>
<string name="close_sheet" msgid="7573152094250666567">"Cerrar hoja"</string>
<string name="default_error_message" msgid="8038256446254964252">"Entrada no válida"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Ventana emergente"</string>
<string name="range_start" msgid="7097486360902471446">"Inicio del intervalo"</string>
<string name="range_end" msgid="5941395253238309765">"Fin del intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-et/strings.xml b/compose/ui/ui/src/androidMain/res/values-et/strings.xml
index 803206d..dd9cae9 100644
--- a/compose/ui/ui/src/androidMain/res/values-et/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-et/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Sule navigeerimismenüü"</string>
<string name="close_sheet" msgid="7573152094250666567">"Sule leht"</string>
<string name="default_error_message" msgid="8038256446254964252">"Sobimatu sisend"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Hüpikaken"</string>
<string name="range_start" msgid="7097486360902471446">"Vahemiku algus"</string>
<string name="range_end" msgid="5941395253238309765">"Vahemiku lõpp"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-eu/strings.xml b/compose/ui/ui/src/androidMain/res/values-eu/strings.xml
index edcc8f2..757bf68 100644
--- a/compose/ui/ui/src/androidMain/res/values-eu/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-eu/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Itxi nabigazio-menua"</string>
<string name="close_sheet" msgid="7573152094250666567">"Itxi orria"</string>
<string name="default_error_message" msgid="8038256446254964252">"Sarrerak ez du balio"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Leiho gainerakorra"</string>
<string name="range_start" msgid="7097486360902471446">"Barrutiaren hasiera"</string>
<string name="range_end" msgid="5941395253238309765">"Barrutiaren amaiera"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-fa/strings.xml b/compose/ui/ui/src/androidMain/res/values-fa/strings.xml
index 39b1a92..c9c25ee 100644
--- a/compose/ui/ui/src/androidMain/res/values-fa/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-fa/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"بستن منوی پیمایش"</string>
<string name="close_sheet" msgid="7573152094250666567">"بستن برگ"</string>
<string name="default_error_message" msgid="8038256446254964252">"ورودی نامعتبر"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"پنجره بالاپر"</string>
<string name="range_start" msgid="7097486360902471446">"شروع محدوده"</string>
<string name="range_end" msgid="5941395253238309765">"پایان محدوده"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-fi/strings.xml b/compose/ui/ui/src/androidMain/res/values-fi/strings.xml
index 5479432..db69277 100644
--- a/compose/ui/ui/src/androidMain/res/values-fi/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-fi/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Sulje navigointivalikko"</string>
<string name="close_sheet" msgid="7573152094250666567">"Sulje taulukko"</string>
<string name="default_error_message" msgid="8038256446254964252">"Virheellinen syöte"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Ponnahdusikkuna"</string>
<string name="range_start" msgid="7097486360902471446">"Alueen alku"</string>
<string name="range_end" msgid="5941395253238309765">"Alueen loppu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-fr-rCA/strings.xml b/compose/ui/ui/src/androidMain/res/values-fr-rCA/strings.xml
index abcbedd..6c9a7ed 100644
--- a/compose/ui/ui/src/androidMain/res/values-fr-rCA/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-fr-rCA/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Fermer le menu de navigation"</string>
<string name="close_sheet" msgid="7573152094250666567">"Fermer la feuille"</string>
<string name="default_error_message" msgid="8038256446254964252">"Entrée incorrecte"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Fenêtre contextuelle"</string>
<string name="range_start" msgid="7097486360902471446">"Début de plage"</string>
<string name="range_end" msgid="5941395253238309765">"Fin de plage"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-fr/strings.xml b/compose/ui/ui/src/androidMain/res/values-fr/strings.xml
index 7f6e679..29894ec 100644
--- a/compose/ui/ui/src/androidMain/res/values-fr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-fr/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Fermer le menu de navigation"</string>
<string name="close_sheet" msgid="7573152094250666567">"Fermer la feuille"</string>
<string name="default_error_message" msgid="8038256446254964252">"Données incorrectes"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Fenêtre pop-up"</string>
<string name="range_start" msgid="7097486360902471446">"Début de plage"</string>
<string name="range_end" msgid="5941395253238309765">"Fin de plage"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-gl/strings.xml b/compose/ui/ui/src/androidMain/res/values-gl/strings.xml
index 045f898..e7a7c7b 100644
--- a/compose/ui/ui/src/androidMain/res/values-gl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-gl/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Pechar menú de navegación"</string>
<string name="close_sheet" msgid="7573152094250666567">"Pechar folla"</string>
<string name="default_error_message" msgid="8038256446254964252">"O texto escrito non é válido"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Ventá emerxente"</string>
<string name="range_start" msgid="7097486360902471446">"Inicio do intervalo"</string>
<string name="range_end" msgid="5941395253238309765">"Fin do intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-gu/strings.xml b/compose/ui/ui/src/androidMain/res/values-gu/strings.xml
index a4cd743..23b9ccb 100644
--- a/compose/ui/ui/src/androidMain/res/values-gu/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-gu/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"નૅવિગેશન મેનૂ બંધ કરો"</string>
<string name="close_sheet" msgid="7573152094250666567">"શીટ બંધ કરો"</string>
<string name="default_error_message" msgid="8038256446254964252">"અમાન્ય ઇનપુટ"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"પૉપ-અપ વિન્ડો"</string>
<string name="range_start" msgid="7097486360902471446">"રેંજની શરૂઆત"</string>
<string name="range_end" msgid="5941395253238309765">"રેંજની સમાપ્તિ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-hi/strings.xml b/compose/ui/ui/src/androidMain/res/values-hi/strings.xml
index fe6906a..103ecfa 100644
--- a/compose/ui/ui/src/androidMain/res/values-hi/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-hi/strings.xml
@@ -31,6 +31,7 @@
<string name="close_drawer" msgid="406453423630273620">"नेविगेशन मेन्यू बंद करें"</string>
<string name="close_sheet" msgid="7573152094250666567">"शीट बंद करें"</string>
<string name="default_error_message" msgid="8038256446254964252">"अमान्य इनपुट"</string>
+ <string name="state_empty" msgid="4139871816613051306">"कोई भी तार नहीं लगा है"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"पॉप-अप विंडो"</string>
<string name="range_start" msgid="7097486360902471446">"रेंज की शुरुआत"</string>
<string name="range_end" msgid="5941395253238309765">"रेंज की सीमा"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-hr/strings.xml b/compose/ui/ui/src/androidMain/res/values-hr/strings.xml
index 1723b39..c168ea0 100644
--- a/compose/ui/ui/src/androidMain/res/values-hr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-hr/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Zatvaranje izbornika za navigaciju"</string>
<string name="close_sheet" msgid="7573152094250666567">"Zatvaranje lista"</string>
<string name="default_error_message" msgid="8038256446254964252">"Nevažeći unos"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Skočni prozor"</string>
<string name="range_start" msgid="7097486360902471446">"Početak raspona"</string>
<string name="range_end" msgid="5941395253238309765">"Kraj raspona"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-hu/strings.xml b/compose/ui/ui/src/androidMain/res/values-hu/strings.xml
index fa50b3e..631ea36 100644
--- a/compose/ui/ui/src/androidMain/res/values-hu/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-hu/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Navigációs menü bezárása"</string>
<string name="close_sheet" msgid="7573152094250666567">"Munkalap bezárása"</string>
<string name="default_error_message" msgid="8038256446254964252">"Érvénytelen adat"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Előugró ablak"</string>
<string name="range_start" msgid="7097486360902471446">"Tartomány kezdete"</string>
<string name="range_end" msgid="5941395253238309765">"Tartomány vége"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-hy/strings.xml b/compose/ui/ui/src/androidMain/res/values-hy/strings.xml
index 3acfd76..598a469 100644
--- a/compose/ui/ui/src/androidMain/res/values-hy/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-hy/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Փակել նավիգացիայի ընտրացանկը"</string>
<string name="close_sheet" msgid="7573152094250666567">"Փակել թերթը"</string>
<string name="default_error_message" msgid="8038256446254964252">"Սխալ ներածում"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Ելնող պատուհան"</string>
<string name="range_start" msgid="7097486360902471446">"Ընդգրկույթի սկիզբ"</string>
<string name="range_end" msgid="5941395253238309765">"Ընդգրկույթի վերջ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-in/strings.xml b/compose/ui/ui/src/androidMain/res/values-in/strings.xml
index 577e85c..650487a 100644
--- a/compose/ui/ui/src/androidMain/res/values-in/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-in/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Tutup menu navigasi"</string>
<string name="close_sheet" msgid="7573152094250666567">"Tutup sheet"</string>
<string name="default_error_message" msgid="8038256446254964252">"Input tidak valid"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Jendela Pop-Up"</string>
<string name="range_start" msgid="7097486360902471446">"Rentang awal"</string>
<string name="range_end" msgid="5941395253238309765">"Rentang akhir"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-is/strings.xml b/compose/ui/ui/src/androidMain/res/values-is/strings.xml
index af87f3b..71b16b1 100644
--- a/compose/ui/ui/src/androidMain/res/values-is/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-is/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Loka yfirlitsvalmynd"</string>
<string name="close_sheet" msgid="7573152094250666567">"Loka blaði"</string>
<string name="default_error_message" msgid="8038256446254964252">"Ógildur innsláttur"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Sprettigluggi"</string>
<string name="range_start" msgid="7097486360902471446">"Upphaf sviðs"</string>
<string name="range_end" msgid="5941395253238309765">"Lok sviðs"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-it/strings.xml b/compose/ui/ui/src/androidMain/res/values-it/strings.xml
index e5727e4..509a24d 100644
--- a/compose/ui/ui/src/androidMain/res/values-it/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-it/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Chiudi il menu di navigazione"</string>
<string name="close_sheet" msgid="7573152094250666567">"Chiudi il foglio"</string>
<string name="default_error_message" msgid="8038256446254964252">"Valore non valido"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Finestra popup"</string>
<string name="range_start" msgid="7097486360902471446">"Inizio intervallo"</string>
<string name="range_end" msgid="5941395253238309765">"Fine intervallo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-iw/strings.xml b/compose/ui/ui/src/androidMain/res/values-iw/strings.xml
index f32fd3a..f567055 100644
--- a/compose/ui/ui/src/androidMain/res/values-iw/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-iw/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"סגירת תפריט הניווט"</string>
<string name="close_sheet" msgid="7573152094250666567">"סגירת הגיליון"</string>
<string name="default_error_message" msgid="8038256446254964252">"הקלט לא תקין"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"חלון קופץ"</string>
<string name="range_start" msgid="7097486360902471446">"תחילת הטווח"</string>
<string name="range_end" msgid="5941395253238309765">"סוף הטווח"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ja/strings.xml b/compose/ui/ui/src/androidMain/res/values-ja/strings.xml
index 0563fb4..cf83e57 100644
--- a/compose/ui/ui/src/androidMain/res/values-ja/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ja/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"ナビゲーションメニューを閉じる"</string>
<string name="close_sheet" msgid="7573152094250666567">"シートを閉じる"</string>
<string name="default_error_message" msgid="8038256446254964252">"入力値が無効です"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"ポップアップウィンドウ"</string>
<string name="range_start" msgid="7097486360902471446">"範囲の先頭"</string>
<string name="range_end" msgid="5941395253238309765">"範囲の末尾"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ka/strings.xml b/compose/ui/ui/src/androidMain/res/values-ka/strings.xml
index d108b025..eb093ba 100644
--- a/compose/ui/ui/src/androidMain/res/values-ka/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ka/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"ნავიგაციის მენიუს დახურვა"</string>
<string name="close_sheet" msgid="7573152094250666567">"ფურცლის დახურვა"</string>
<string name="default_error_message" msgid="8038256446254964252">"შენატანი არასწორია"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"ამომხტარი ფანჯარა"</string>
<string name="range_start" msgid="7097486360902471446">"დიაპაზონის დასაწყისი"</string>
<string name="range_end" msgid="5941395253238309765">"დიაპაზონის დასასრული"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-kk/strings.xml b/compose/ui/ui/src/androidMain/res/values-kk/strings.xml
index 69c768d..d21df0b 100644
--- a/compose/ui/ui/src/androidMain/res/values-kk/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-kk/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Навигация мәзірін жабу"</string>
<string name="close_sheet" msgid="7573152094250666567">"Парақты жабу"</string>
<string name="default_error_message" msgid="8038256446254964252">"Енгізілген мән жарамсыз."</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Қалқымалы терезе"</string>
<string name="range_start" msgid="7097486360902471446">"Аралықтың басы"</string>
<string name="range_end" msgid="5941395253238309765">"Аралықтың соңы"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-km/strings.xml b/compose/ui/ui/src/androidMain/res/values-km/strings.xml
index 5257840..e10e501 100644
--- a/compose/ui/ui/src/androidMain/res/values-km/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-km/strings.xml
@@ -31,6 +31,7 @@
<string name="close_drawer" msgid="406453423630273620">"បិទម៉ឺនុយរុករក"</string>
<string name="close_sheet" msgid="7573152094250666567">"បិទសន្លឹក"</string>
<string name="default_error_message" msgid="8038256446254964252">"ការបញ្ចូលមិនត្រឹមត្រូវ"</string>
+ <string name="state_empty" msgid="4139871816613051306">"ទទេ"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"វិនដូលោតឡើង"</string>
<string name="range_start" msgid="7097486360902471446">"ចំណុចចាប់ផ្ដើម"</string>
<string name="range_end" msgid="5941395253238309765">"ចំណុចបញ្ចប់"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-kn/strings.xml b/compose/ui/ui/src/androidMain/res/values-kn/strings.xml
index 271b86a..5eb9b0b 100644
--- a/compose/ui/ui/src/androidMain/res/values-kn/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-kn/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"ನ್ಯಾವಿಗೇಷನ್ ಮೆನು ಮುಚ್ಚಿರಿ"</string>
<string name="close_sheet" msgid="7573152094250666567">"ಶೀಟ್ ಮುಚ್ಚಿರಿ"</string>
<string name="default_error_message" msgid="8038256446254964252">"ಅಮಾನ್ಯ ಇನ್ಪುಟ್"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"ಪಾಪ್-ಅಪ್ ವಿಂಡೋ"</string>
<string name="range_start" msgid="7097486360902471446">"ಶ್ರೇಣಿಯ ಪ್ರಾರಂಭ"</string>
<string name="range_end" msgid="5941395253238309765">"ಶ್ರೇಣಿಯ ಅಂತ್ಯ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ko/strings.xml b/compose/ui/ui/src/androidMain/res/values-ko/strings.xml
index b486704..d452c4d82 100644
--- a/compose/ui/ui/src/androidMain/res/values-ko/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ko/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"탐색 메뉴 닫기"</string>
<string name="close_sheet" msgid="7573152094250666567">"시트 닫기"</string>
<string name="default_error_message" msgid="8038256446254964252">"입력이 잘못됨"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"팝업 창"</string>
<string name="range_start" msgid="7097486360902471446">"범위 시작"</string>
<string name="range_end" msgid="5941395253238309765">"범위 끝"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ky/strings.xml b/compose/ui/ui/src/androidMain/res/values-ky/strings.xml
index 686a577..4c6591e 100644
--- a/compose/ui/ui/src/androidMain/res/values-ky/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ky/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Чабыттоо менюсун жабуу"</string>
<string name="close_sheet" msgid="7573152094250666567">"Баракты жабуу"</string>
<string name="default_error_message" msgid="8038256446254964252">"Киргизилген маалымат жараксыз"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Калкыма терезе"</string>
<string name="range_start" msgid="7097486360902471446">"Диапазондун башы"</string>
<string name="range_end" msgid="5941395253238309765">"Диапазондун аягы"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-lo/strings.xml b/compose/ui/ui/src/androidMain/res/values-lo/strings.xml
index 7c36d6e..c201f11 100644
--- a/compose/ui/ui/src/androidMain/res/values-lo/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-lo/strings.xml
@@ -31,6 +31,7 @@
<string name="close_drawer" msgid="406453423630273620">"ປິດເມນູການນຳທາງ"</string>
<string name="close_sheet" msgid="7573152094250666567">"ປິດຊີດ"</string>
<string name="default_error_message" msgid="8038256446254964252">"ຂໍ້ມູນທີ່ປ້ອນເຂົ້າບໍ່ຖືກຕ້ອງ"</string>
+ <string name="state_empty" msgid="4139871816613051306">"ຫວ່າງເປົ່າ"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"ໜ້າຈໍປັອບອັບ"</string>
<string name="range_start" msgid="7097486360902471446">"ເລີ່ມຕົ້ນໄລຍະ"</string>
<string name="range_end" msgid="5941395253238309765">"ສິ້ນສຸດໄລຍະ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-lt/strings.xml b/compose/ui/ui/src/androidMain/res/values-lt/strings.xml
index 7ad81ab..9da20ee 100644
--- a/compose/ui/ui/src/androidMain/res/values-lt/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-lt/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Uždaryti naršymo meniu"</string>
<string name="close_sheet" msgid="7573152094250666567">"Uždaryti lapą"</string>
<string name="default_error_message" msgid="8038256446254964252">"Netinkama įvestis"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Iššokantysis langas"</string>
<string name="range_start" msgid="7097486360902471446">"Diapazono pradžia"</string>
<string name="range_end" msgid="5941395253238309765">"Diapazono pabaiga"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-lv/strings.xml b/compose/ui/ui/src/androidMain/res/values-lv/strings.xml
index 4baf5f9..4a5726e 100644
--- a/compose/ui/ui/src/androidMain/res/values-lv/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-lv/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Aizvērt navigācijas izvēlni"</string>
<string name="close_sheet" msgid="7573152094250666567">"Aizvērt izklājlapu"</string>
<string name="default_error_message" msgid="8038256446254964252">"Nederīga ievade"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Uznirstošais logs"</string>
<string name="range_start" msgid="7097486360902471446">"Diapazona sākums"</string>
<string name="range_end" msgid="5941395253238309765">"Diapazona beigas"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-mk/strings.xml b/compose/ui/ui/src/androidMain/res/values-mk/strings.xml
index b3f14062..6580504 100644
--- a/compose/ui/ui/src/androidMain/res/values-mk/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-mk/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Затворете го менито за навигација"</string>
<string name="close_sheet" msgid="7573152094250666567">"Затворете го листот"</string>
<string name="default_error_message" msgid="8038256446254964252">"Неважечки запис"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Скокачки прозорец"</string>
<string name="range_start" msgid="7097486360902471446">"Почеток на опсегот"</string>
<string name="range_end" msgid="5941395253238309765">"Крај на опсегот"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ml/strings.xml b/compose/ui/ui/src/androidMain/res/values-ml/strings.xml
index 7e1a80d..0514441 100644
--- a/compose/ui/ui/src/androidMain/res/values-ml/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ml/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"നാവിഗേഷൻ മെനു അടയ്ക്കുക"</string>
<string name="close_sheet" msgid="7573152094250666567">"ഷീറ്റ് അടയ്ക്കുക"</string>
<string name="default_error_message" msgid="8038256446254964252">"ഇൻപുട്ട് അസാധുവാണ്"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"പോപ്പ്-അപ്പ് വിൻഡോ"</string>
<string name="range_start" msgid="7097486360902471446">"ശ്രേണിയുടെ ആരംഭം"</string>
<string name="range_end" msgid="5941395253238309765">"ശ്രേണിയുടെ അവസാനം"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-mn/strings.xml b/compose/ui/ui/src/androidMain/res/values-mn/strings.xml
index 9fd17b1..df96b02 100644
--- a/compose/ui/ui/src/androidMain/res/values-mn/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-mn/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Навигацын цэсийг хаах"</string>
<string name="close_sheet" msgid="7573152094250666567">"Хүснэгтийг хаах"</string>
<string name="default_error_message" msgid="8038256446254964252">"Буруу оролт"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Попап цонх"</string>
<string name="range_start" msgid="7097486360902471446">"Мужийн эхлэл"</string>
<string name="range_end" msgid="5941395253238309765">"Мужийн төгсгөл"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-mr/strings.xml b/compose/ui/ui/src/androidMain/res/values-mr/strings.xml
index ad44056..54aaab6 100644
--- a/compose/ui/ui/src/androidMain/res/values-mr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-mr/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"नेव्हिगेशन मेनू बंद करा"</string>
<string name="close_sheet" msgid="7573152094250666567">"शीट बंद करा"</string>
<string name="default_error_message" msgid="8038256446254964252">"इनपुट चुकीचे आहे"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"पॉप-अप विंडो"</string>
<string name="range_start" msgid="7097486360902471446">"रेंजची सुरुवात"</string>
<string name="range_end" msgid="5941395253238309765">"रेंजचा शेवट"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ms/strings.xml b/compose/ui/ui/src/androidMain/res/values-ms/strings.xml
index d33a81a..72af8eb 100644
--- a/compose/ui/ui/src/androidMain/res/values-ms/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ms/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Tutup menu navigasi"</string>
<string name="close_sheet" msgid="7573152094250666567">"Tutup helaian"</string>
<string name="default_error_message" msgid="8038256446254964252">"Input tidak sah"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Tetingkap Timbul"</string>
<string name="range_start" msgid="7097486360902471446">"Permulaan julat"</string>
<string name="range_end" msgid="5941395253238309765">"Penghujung julat"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-my/strings.xml b/compose/ui/ui/src/androidMain/res/values-my/strings.xml
index 1ef3aab..a44f8ed 100644
--- a/compose/ui/ui/src/androidMain/res/values-my/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-my/strings.xml
@@ -31,6 +31,7 @@
<string name="close_drawer" msgid="406453423630273620">"လမ်းညွှန် မီနူး ပိတ်ရန်"</string>
<string name="close_sheet" msgid="7573152094250666567">"စာမျက်နှာ ပိတ်ရန်"</string>
<string name="default_error_message" msgid="8038256446254964252">"ထည့်သွင်းမှု မမှန်ကန်ပါ"</string>
+ <string name="state_empty" msgid="4139871816613051306">"မရှိပါ"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"ပေါ့ပ်အပ် ဝင်းဒိုး"</string>
<string name="range_start" msgid="7097486360902471446">"အပိုင်းအခြား အစ"</string>
<string name="range_end" msgid="5941395253238309765">"အပိုင်းအခြား အဆုံး"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-nb/strings.xml b/compose/ui/ui/src/androidMain/res/values-nb/strings.xml
index 1395a49..2a4c5d6 100644
--- a/compose/ui/ui/src/androidMain/res/values-nb/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-nb/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Lukk navigasjonsmenyen"</string>
<string name="close_sheet" msgid="7573152094250666567">"Lukk arket"</string>
<string name="default_error_message" msgid="8038256446254964252">"Ugyldige inndata"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Forgrunnsvindu"</string>
<string name="range_start" msgid="7097486360902471446">"Områdestart"</string>
<string name="range_end" msgid="5941395253238309765">"Områdeslutt"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ne/strings.xml b/compose/ui/ui/src/androidMain/res/values-ne/strings.xml
index 53b2ce10..f8a9002 100644
--- a/compose/ui/ui/src/androidMain/res/values-ne/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ne/strings.xml
@@ -31,6 +31,7 @@
<string name="close_drawer" msgid="406453423630273620">"नेभिगेसन मेनु बन्द गर्नुहोस्"</string>
<string name="close_sheet" msgid="7573152094250666567">"पाना बन्द गर्नुहोस्"</string>
<string name="default_error_message" msgid="8038256446254964252">"अवैद्य इन्पुट"</string>
+ <string name="state_empty" msgid="4139871816613051306">"खाली"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"पपअप विन्डो"</string>
<string name="range_start" msgid="7097486360902471446">"दायराको सुरुवात बिन्दु"</string>
<string name="range_end" msgid="5941395253238309765">"दायराको अन्तिम बिन्दु"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-nl/strings.xml b/compose/ui/ui/src/androidMain/res/values-nl/strings.xml
index c2a6f5b..129ccb0 100644
--- a/compose/ui/ui/src/androidMain/res/values-nl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-nl/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Navigatiemenu sluiten"</string>
<string name="close_sheet" msgid="7573152094250666567">"Blad sluiten"</string>
<string name="default_error_message" msgid="8038256446254964252">"Ongeldige invoer"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Pop-upvenster"</string>
<string name="range_start" msgid="7097486360902471446">"Start bereik"</string>
<string name="range_end" msgid="5941395253238309765">"Einde bereik"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-or/strings.xml b/compose/ui/ui/src/androidMain/res/values-or/strings.xml
index 32a9c02..6773c55 100644
--- a/compose/ui/ui/src/androidMain/res/values-or/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-or/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"ନାଭିଗେସନ୍ ମେନୁ ବନ୍ଦ କରନ୍ତୁ"</string>
<string name="close_sheet" msgid="7573152094250666567">"ସିଟ୍ ବନ୍ଦ କରନ୍ତୁ"</string>
<string name="default_error_message" msgid="8038256446254964252">"ଅବୈଧ ଇନପୁଟ୍"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"ପପ୍-ଅପ୍ ୱିଣ୍ଡୋ"</string>
<string name="range_start" msgid="7097486360902471446">"ରେଞ୍ଜ ଆରମ୍ଭ"</string>
<string name="range_end" msgid="5941395253238309765">"ରେଞ୍ଜ ଶେଷ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pa/strings.xml b/compose/ui/ui/src/androidMain/res/values-pa/strings.xml
index 1a25393..a88164a 100644
--- a/compose/ui/ui/src/androidMain/res/values-pa/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pa/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"ਨੈਵੀਗੇਸ਼ਨ ਮੀਨੂ ਬੰਦ ਕਰੋ"</string>
<string name="close_sheet" msgid="7573152094250666567">"ਸ਼ੀਟ ਬੰਦ ਕਰੋ"</string>
<string name="default_error_message" msgid="8038256446254964252">"ਅਵੈਧ ਇਨਪੁੱਟ"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"ਪੌਪ-ਅੱਪ ਵਿੰਡੋ"</string>
<string name="range_start" msgid="7097486360902471446">"ਰੇਂਜ ਸ਼ੁਰੂ"</string>
<string name="range_end" msgid="5941395253238309765">"ਰੇਂਜ ਸਮਾਪਤ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pl/strings.xml b/compose/ui/ui/src/androidMain/res/values-pl/strings.xml
index 6029183..aaff133 100644
--- a/compose/ui/ui/src/androidMain/res/values-pl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pl/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Zamknij menu nawigacyjne"</string>
<string name="close_sheet" msgid="7573152094250666567">"Zamknij arkusz"</string>
<string name="default_error_message" msgid="8038256446254964252">"Nieprawidłowe dane wejściowe"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Wyskakujące okienko"</string>
<string name="range_start" msgid="7097486360902471446">"Początek zakresu"</string>
<string name="range_end" msgid="5941395253238309765">"Koniec zakresu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pt-rBR/strings.xml b/compose/ui/ui/src/androidMain/res/values-pt-rBR/strings.xml
index 0c70403..e5d9ff3 100644
--- a/compose/ui/ui/src/androidMain/res/values-pt-rBR/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pt-rBR/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Fechar menu de navegação"</string>
<string name="close_sheet" msgid="7573152094250666567">"Fechar planilha"</string>
<string name="default_error_message" msgid="8038256446254964252">"Entrada inválida"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Janela pop-up"</string>
<string name="range_start" msgid="7097486360902471446">"Início do intervalo"</string>
<string name="range_end" msgid="5941395253238309765">"Fim do intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pt-rPT/strings.xml b/compose/ui/ui/src/androidMain/res/values-pt-rPT/strings.xml
index c77ce8e..be24761 100644
--- a/compose/ui/ui/src/androidMain/res/values-pt-rPT/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pt-rPT/strings.xml
@@ -31,6 +31,7 @@
<string name="close_drawer" msgid="406453423630273620">"Fechar menu de navegação"</string>
<string name="close_sheet" msgid="7573152094250666567">"Fechar folha"</string>
<string name="default_error_message" msgid="8038256446254964252">"Entrada inválida"</string>
+ <string name="state_empty" msgid="4139871816613051306">"Vazio"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"Janela pop-up"</string>
<string name="range_start" msgid="7097486360902471446">"Início do intervalo"</string>
<string name="range_end" msgid="5941395253238309765">"Fim do intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pt/strings.xml b/compose/ui/ui/src/androidMain/res/values-pt/strings.xml
index 0c70403..e5d9ff3 100644
--- a/compose/ui/ui/src/androidMain/res/values-pt/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pt/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Fechar menu de navegação"</string>
<string name="close_sheet" msgid="7573152094250666567">"Fechar planilha"</string>
<string name="default_error_message" msgid="8038256446254964252">"Entrada inválida"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Janela pop-up"</string>
<string name="range_start" msgid="7097486360902471446">"Início do intervalo"</string>
<string name="range_end" msgid="5941395253238309765">"Fim do intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ro/strings.xml b/compose/ui/ui/src/androidMain/res/values-ro/strings.xml
index c2da926..7e8a0dc 100644
--- a/compose/ui/ui/src/androidMain/res/values-ro/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ro/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Închide meniul de navigare"</string>
<string name="close_sheet" msgid="7573152094250666567">"Închide foaia"</string>
<string name="default_error_message" msgid="8038256446254964252">"Intrare nevalidă"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Fereastră pop-up"</string>
<string name="range_start" msgid="7097486360902471446">"Început de interval"</string>
<string name="range_end" msgid="5941395253238309765">"Sfârșit de interval"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ru/strings.xml b/compose/ui/ui/src/androidMain/res/values-ru/strings.xml
index 3b9a833..218c2c1 100644
--- a/compose/ui/ui/src/androidMain/res/values-ru/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ru/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Закрыть меню навигации"</string>
<string name="close_sheet" msgid="7573152094250666567">"Закрыть лист"</string>
<string name="default_error_message" msgid="8038256446254964252">"Неправильный ввод"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Всплывающее окно"</string>
<string name="range_start" msgid="7097486360902471446">"Начало диапазона"</string>
<string name="range_end" msgid="5941395253238309765">"Конец диапазона"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-si/strings.xml b/compose/ui/ui/src/androidMain/res/values-si/strings.xml
index ebf3e39..787156c 100644
--- a/compose/ui/ui/src/androidMain/res/values-si/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-si/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"සංචාලන මෙනුව වසන්න"</string>
<string name="close_sheet" msgid="7573152094250666567">"පත්රය වසන්න"</string>
<string name="default_error_message" msgid="8038256446254964252">"වලංගු නොවන ආදානයකි"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"උත්පතන කවුළුව"</string>
<string name="range_start" msgid="7097486360902471446">"පරාස ආරම්භය"</string>
<string name="range_end" msgid="5941395253238309765">"පරාස අන්තය"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sk/strings.xml b/compose/ui/ui/src/androidMain/res/values-sk/strings.xml
index d576b04..0b28e8c 100644
--- a/compose/ui/ui/src/androidMain/res/values-sk/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sk/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Zavrieť navigačnú ponuku"</string>
<string name="close_sheet" msgid="7573152094250666567">"Zavrieť hárok"</string>
<string name="default_error_message" msgid="8038256446254964252">"Neplatný vstup"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Vyskakovacie okno"</string>
<string name="range_start" msgid="7097486360902471446">"Začiatok rozsahu"</string>
<string name="range_end" msgid="5941395253238309765">"Koniec rozsahu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sl/strings.xml b/compose/ui/ui/src/androidMain/res/values-sl/strings.xml
index 02e0df2..eb9301f5 100644
--- a/compose/ui/ui/src/androidMain/res/values-sl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sl/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Zapri meni za krmarjenje"</string>
<string name="close_sheet" msgid="7573152094250666567">"Zapri list"</string>
<string name="default_error_message" msgid="8038256446254964252">"Neveljaven vnos"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Pojavno okno"</string>
<string name="range_start" msgid="7097486360902471446">"Začetek razpona"</string>
<string name="range_end" msgid="5941395253238309765">"Konec razpona"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sq/strings.xml b/compose/ui/ui/src/androidMain/res/values-sq/strings.xml
index 7d26d4c..a188d25 100644
--- a/compose/ui/ui/src/androidMain/res/values-sq/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sq/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Mbyll menynë e navigimit"</string>
<string name="close_sheet" msgid="7573152094250666567">"Mbyll fletën"</string>
<string name="default_error_message" msgid="8038256446254964252">"Hyrje e pavlefshme"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Dritare kërcyese"</string>
<string name="range_start" msgid="7097486360902471446">"Fillimi i diapazonit"</string>
<string name="range_end" msgid="5941395253238309765">"Fundi i diapazonit"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sr/strings.xml b/compose/ui/ui/src/androidMain/res/values-sr/strings.xml
index 3502abf..9708e29 100644
--- a/compose/ui/ui/src/androidMain/res/values-sr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sr/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Затвори мени за навигацију"</string>
<string name="close_sheet" msgid="7573152094250666567">"Затворите табелу"</string>
<string name="default_error_message" msgid="8038256446254964252">"Унос је неважећи"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Искачући прозор"</string>
<string name="range_start" msgid="7097486360902471446">"Почетак опсега"</string>
<string name="range_end" msgid="5941395253238309765">"Крај опсега"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sv/strings.xml b/compose/ui/ui/src/androidMain/res/values-sv/strings.xml
index f005131b..0bf933b 100644
--- a/compose/ui/ui/src/androidMain/res/values-sv/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sv/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Stäng navigeringsmenyn"</string>
<string name="close_sheet" msgid="7573152094250666567">"Stäng kalkylarket"</string>
<string name="default_error_message" msgid="8038256446254964252">"Ogiltiga indata"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Popup-fönster"</string>
<string name="range_start" msgid="7097486360902471446">"Intervallets början"</string>
<string name="range_end" msgid="5941395253238309765">"Intervallets slut"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sw/strings.xml b/compose/ui/ui/src/androidMain/res/values-sw/strings.xml
index 42d3e1f..98421b2 100644
--- a/compose/ui/ui/src/androidMain/res/values-sw/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sw/strings.xml
@@ -31,6 +31,7 @@
<string name="close_drawer" msgid="406453423630273620">"Funga menyu ya kusogeza"</string>
<string name="close_sheet" msgid="7573152094250666567">"Funga laha"</string>
<string name="default_error_message" msgid="8038256446254964252">"Ulichoweka si sahihi"</string>
+ <string name="state_empty" msgid="4139871816613051306">"Tupu"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"Dirisha Ibukizi"</string>
<string name="range_start" msgid="7097486360902471446">"Mwanzo wa masafa"</string>
<string name="range_end" msgid="5941395253238309765">"Mwisho wa masafa"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ta/strings.xml b/compose/ui/ui/src/androidMain/res/values-ta/strings.xml
index 8e584e7..cbaf6f8 100644
--- a/compose/ui/ui/src/androidMain/res/values-ta/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ta/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"வழிசெலுத்தல் மெனுவை மூடும்"</string>
<string name="close_sheet" msgid="7573152094250666567">"ஷீட்டை மூடும்"</string>
<string name="default_error_message" msgid="8038256446254964252">"தவறான உள்ளீடு"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"பாப்-அப் சாளரம்"</string>
<string name="range_start" msgid="7097486360902471446">"வரம்பு தொடக்கம்"</string>
<string name="range_end" msgid="5941395253238309765">"வரம்பு முடிவு"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-te/strings.xml b/compose/ui/ui/src/androidMain/res/values-te/strings.xml
index cecac59e..fec67d4e 100644
--- a/compose/ui/ui/src/androidMain/res/values-te/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-te/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"నావిగేషన్ మెనూను మూసివేయండి"</string>
<string name="close_sheet" msgid="7573152094250666567">"షీట్ను మూసివేయండి"</string>
<string name="default_error_message" msgid="8038256446254964252">"ఇన్పుట్ చెల్లదు"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"పాప్-అప్ విండో"</string>
<string name="range_start" msgid="7097486360902471446">"పరిధి ప్రారంభమయింది"</string>
<string name="range_end" msgid="5941395253238309765">"పరిధి ముగిసింది"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-th/strings.xml b/compose/ui/ui/src/androidMain/res/values-th/strings.xml
index 3ceed7d..1391214 100644
--- a/compose/ui/ui/src/androidMain/res/values-th/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-th/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"ปิดเมนูการนำทาง"</string>
<string name="close_sheet" msgid="7573152094250666567">"ปิดชีต"</string>
<string name="default_error_message" msgid="8038256446254964252">"อินพุตไม่ถูกต้อง"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"หน้าต่างป๊อปอัป"</string>
<string name="range_start" msgid="7097486360902471446">"จุดเริ่มต้นของช่วง"</string>
<string name="range_end" msgid="5941395253238309765">"จุดสิ้นสุดของช่วง"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-tl/strings.xml b/compose/ui/ui/src/androidMain/res/values-tl/strings.xml
index 9ab42b2d..af0b7d2 100644
--- a/compose/ui/ui/src/androidMain/res/values-tl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-tl/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Isara ang menu ng navigation"</string>
<string name="close_sheet" msgid="7573152094250666567">"Isara ang sheet"</string>
<string name="default_error_message" msgid="8038256446254964252">"Invalid na input"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Window ng Pop-Up"</string>
<string name="range_start" msgid="7097486360902471446">"Simula ng range"</string>
<string name="range_end" msgid="5941395253238309765">"Katapusan ng range"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-tr/strings.xml b/compose/ui/ui/src/androidMain/res/values-tr/strings.xml
index 909d057..aa8f590 100644
--- a/compose/ui/ui/src/androidMain/res/values-tr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-tr/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Gezinme menüsünü kapat"</string>
<string name="close_sheet" msgid="7573152094250666567">"Sayfayı kapat"</string>
<string name="default_error_message" msgid="8038256446254964252">"Geçersiz giriş"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up Pencere"</string>
<string name="range_start" msgid="7097486360902471446">"Aralık başlangıcı"</string>
<string name="range_end" msgid="5941395253238309765">"Aralık sonu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-uk/strings.xml b/compose/ui/ui/src/androidMain/res/values-uk/strings.xml
index 2cec971..b93fba07 100644
--- a/compose/ui/ui/src/androidMain/res/values-uk/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-uk/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Закрити меню навігації"</string>
<string name="close_sheet" msgid="7573152094250666567">"Закрити аркуш"</string>
<string name="default_error_message" msgid="8038256446254964252">"Введено недійсні дані"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Спливаюче вікно"</string>
<string name="range_start" msgid="7097486360902471446">"Початок діапазону"</string>
<string name="range_end" msgid="5941395253238309765">"Кінець діапазону"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ur/strings.xml b/compose/ui/ui/src/androidMain/res/values-ur/strings.xml
index e9c0509..7ee15a1 100644
--- a/compose/ui/ui/src/androidMain/res/values-ur/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ur/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"نیویگیشن مینیو بند کریں"</string>
<string name="close_sheet" msgid="7573152094250666567">"شیٹ بند کریں"</string>
<string name="default_error_message" msgid="8038256446254964252">"غلط ان پٹ"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"پاپ اپ ونڈو"</string>
<string name="range_start" msgid="7097486360902471446">"رینج کی شروعات"</string>
<string name="range_end" msgid="5941395253238309765">"رینج کا اختتام"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-uz/strings.xml b/compose/ui/ui/src/androidMain/res/values-uz/strings.xml
index 7115c76..8ccc776 100644
--- a/compose/ui/ui/src/androidMain/res/values-uz/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-uz/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Navigatsiya menyusini yopish"</string>
<string name="close_sheet" msgid="7573152094250666567">"Varaqni yopish"</string>
<string name="default_error_message" msgid="8038256446254964252">"Kiritilgan axborot xato"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Qalqib chiquvchi oyna"</string>
<string name="range_start" msgid="7097486360902471446">"Oraliq boshi"</string>
<string name="range_end" msgid="5941395253238309765">"Oraliq oxiri"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-vi/strings.xml b/compose/ui/ui/src/androidMain/res/values-vi/strings.xml
index a7728063..1313dc4 100644
--- a/compose/ui/ui/src/androidMain/res/values-vi/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-vi/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Đóng trình đơn điều hướng"</string>
<string name="close_sheet" msgid="7573152094250666567">"Đóng trang tính"</string>
<string name="default_error_message" msgid="8038256446254964252">"Giá trị nhập không hợp lệ"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Cửa sổ bật lên"</string>
<string name="range_start" msgid="7097486360902471446">"Điểm bắt đầu phạm vi"</string>
<string name="range_end" msgid="5941395253238309765">"Điểm kết thúc phạm vi"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-zh-rCN/strings.xml b/compose/ui/ui/src/androidMain/res/values-zh-rCN/strings.xml
index 2145293..b7031c6 100644
--- a/compose/ui/ui/src/androidMain/res/values-zh-rCN/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-zh-rCN/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"关闭导航菜单"</string>
<string name="close_sheet" msgid="7573152094250666567">"关闭工作表"</string>
<string name="default_error_message" msgid="8038256446254964252">"输入无效"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"弹出式窗口"</string>
<string name="range_start" msgid="7097486360902471446">"范围起点"</string>
<string name="range_end" msgid="5941395253238309765">"范围终点"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-zh-rHK/strings.xml b/compose/ui/ui/src/androidMain/res/values-zh-rHK/strings.xml
index a5804a8..155d6d3 100644
--- a/compose/ui/ui/src/androidMain/res/values-zh-rHK/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-zh-rHK/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"閂導覽選單"</string>
<string name="close_sheet" msgid="7573152094250666567">"閂表單"</string>
<string name="default_error_message" msgid="8038256446254964252">"輸入嘅資料無效"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"彈出式視窗"</string>
<string name="range_start" msgid="7097486360902471446">"範圍開始"</string>
<string name="range_end" msgid="5941395253238309765">"範圍結束"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-zh-rTW/strings.xml b/compose/ui/ui/src/androidMain/res/values-zh-rTW/strings.xml
index 36aa760..198d101 100644
--- a/compose/ui/ui/src/androidMain/res/values-zh-rTW/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-zh-rTW/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"關閉導覽選單"</string>
<string name="close_sheet" msgid="7573152094250666567">"關閉功能表"</string>
<string name="default_error_message" msgid="8038256446254964252">"輸入內容無效"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"彈出式視窗"</string>
<string name="range_start" msgid="7097486360902471446">"範圍起點"</string>
<string name="range_end" msgid="5941395253238309765">"範圍終點"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-zu/strings.xml b/compose/ui/ui/src/androidMain/res/values-zu/strings.xml
index 3237ddb..54fca1b 100644
--- a/compose/ui/ui/src/androidMain/res/values-zu/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-zu/strings.xml
@@ -31,6 +31,8 @@
<string name="close_drawer" msgid="406453423630273620">"Vala imenyu yokuzulazula"</string>
<string name="close_sheet" msgid="7573152094250666567">"Vala ishidi"</string>
<string name="default_error_message" msgid="8038256446254964252">"Okufakwayo okungalungile"</string>
+ <!-- no translation found for state_empty (4139871816613051306) -->
+ <skip />
<string name="default_popup_window_title" msgid="6312721426453364202">"Iwindi Lesikhashana"</string>
<string name="range_start" msgid="7097486360902471446">"Ukuqala kobubanzi"</string>
<string name="range_end" msgid="5941395253238309765">"Umkhawulo wobubanzi"</string>
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestSharePointerInputWithSiblingTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestSharePointerInputWithSiblingTest.kt
new file mode 100644
index 0000000..36b2ec8
--- /dev/null
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestSharePointerInputWithSiblingTest.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2021 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.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.unit.IntSize
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class HitTestSharePointerInputWithSiblingTest {
+ @Test
+ fun hitTest_sharePointerWithSibling() {
+ val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+ val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+ val outerNode = LayoutNode(0, 0, 1, 1) {
+ childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+ childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+ }
+
+ val hit = mutableListOf<Modifier.Node>()
+
+ outerNode.hitTest(Offset(0f, 0f), hit)
+
+ assertThat(hit).isEqualTo(listOf(pointerInputModifier2, pointerInputModifier1))
+ }
+
+ @Test
+ fun hitTest_sharePointerWithSibling_utilFirstNodeNotSharing() {
+ val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+ val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+ val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+ val outerNode = LayoutNode(0, 0, 1, 1) {
+ childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+ childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+ childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+ }
+
+ val hit = mutableListOf<Modifier.Node>()
+
+ outerNode.hitTest(Offset(0f, 0f), hit)
+
+ assertThat(hit).isEqualTo(listOf(pointerInputModifier3, pointerInputModifier2))
+ }
+
+ @Test
+ fun hitTest_sharePointerWithSibling_whenParentDisallowShare() {
+ val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+ val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+ val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+ val outerNode = LayoutNode(0, 0, 1, 1) {
+ childNode(0, 0, 1, 1, pointerInputModifier1.toModifier()) {
+ childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+ childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+ }
+ }
+
+ val hit = mutableListOf<Modifier.Node>()
+
+ outerNode.hitTest(Offset(0f, 0f), hit)
+
+ // The parent node doesn't share pointer events, the two children can still share events.
+ assertThat(hit).isEqualTo(
+ listOf(pointerInputModifier1, pointerInputModifier3, pointerInputModifier2)
+ )
+ }
+
+ @Test
+ fun hitTest_sharePointerWithSiblingTrue_shareWithCousin() {
+ val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+ val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+ val outerNode = LayoutNode(0, 0, 1, 1) {
+ childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+ childNode(0, 0, 1, 1) {
+ childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+ }
+ }
+
+ val hit = mutableListOf<Modifier.Node>()
+
+ outerNode.hitTest(Offset(0f, 0f), hit)
+
+ assertThat(hit).isEqualTo(listOf(pointerInputModifier2, pointerInputModifier1))
+ }
+
+ @Test
+ fun hitTest_parentDisallowShare_notShareWithCousin() {
+ val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+ val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+ val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+ val outerNode = LayoutNode(0, 0, 1, 1) {
+ childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+ childNode(0, 0, 1, 1, pointerInputModifier2.toModifier()) {
+ childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+ }
+ }
+
+ val hit = mutableListOf<Modifier.Node>()
+
+ outerNode.hitTest(Offset(0f, 0f), hit)
+
+ // PointerInputModifier1 can't receive events because pointerInputModifier2 doesn't share.
+ assertThat(hit).isEqualTo(listOf(pointerInputModifier2, pointerInputModifier3))
+ }
+
+ @Test
+ fun hitTest_sharePointerWithCousin_untilFirstNodeNotSharing() {
+ val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+ val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+ val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+ val outerNode = LayoutNode(0, 0, 1, 1) {
+ childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+ childNode(0, 0, 1, 1) {
+ childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+ }
+ childNode(0, 0, 1, 1) {
+ childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+ }
+ }
+
+ val hit = mutableListOf<Modifier.Node>()
+
+ outerNode.hitTest(Offset(0f, 0f), hit)
+
+ assertThat(hit).isEqualTo(listOf(pointerInputModifier3, pointerInputModifier2))
+ }
+}
+
+private fun LayoutNode(
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ block: LayoutNode.() -> Unit
+): LayoutNode {
+ val root = LayoutNode(left, top, right, bottom).apply {
+ attach(MockOwner())
+ }
+
+ block.invoke(root)
+ return root
+}
+
+private fun LayoutNode.childNode(
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ modifier: Modifier = Modifier,
+ block: LayoutNode.() -> Unit = {}
+): LayoutNode {
+ val layoutNode = LayoutNode(left, top, right, bottom, modifier)
+ add(layoutNode)
+ layoutNode.onNodePlaced()
+ block.invoke(layoutNode)
+ return layoutNode
+}
+
+private fun FakePointerInputModifierNode.toModifier(): Modifier {
+ return object : ModifierNodeElement<FakePointerInputModifierNode>() {
+ override fun create(): FakePointerInputModifierNode = this@toModifier
+
+ override fun update(node: FakePointerInputModifierNode) { }
+
+ override fun hashCode(): Int {
+ return if (this@toModifier.sharePointerWithSiblings) 1 else 0
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return this@toModifier.sharePointerWithSiblings
+ }
+ }
+}
+
+private class FakePointerInputModifierNode(
+ var sharePointerWithSiblings: Boolean = false
+) : Modifier.Node(), PointerInputModifierNode {
+ override fun onPointerEvent(
+ pointerEvent: PointerEvent,
+ pass: PointerEventPass,
+ bounds: IntSize
+ ) {}
+
+ override fun onCancelPointerInput() {}
+
+ override fun sharePointerInputWithSiblings(): Boolean = this.sharePointerWithSiblings
+}
+
+private fun LayoutNode.hitTest(
+ pointerPosition: Offset,
+ hitPointerInputFilters: MutableList<Modifier.Node>,
+ isTouchEvent: Boolean = false
+) {
+ val hitTestResult = HitTestResult()
+ hitTest(pointerPosition, hitTestResult, isTouchEvent)
+ hitPointerInputFilters.addAll(hitTestResult)
+}
+
+private fun LayoutNode.onNodePlaced() = measurePassDelegate.onNodePlaced()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index e9a8947..f387981 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -49,6 +49,7 @@
/**
* Paint the content using [painter].
*
+ * @param painter [Painter] to be drawn by this [Modifier]
* @param sizeToIntrinsics `true` to size the element relative to [Painter.intrinsicSize]
* @param alignment specifies alignment of the [painter] relative to content
* @param contentScale strategy for scaling [painter] if its size does not match the content size
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
index f3703e0..e48553d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
@@ -82,6 +82,9 @@
* Use a [androidx.compose.ui.zIndex] modifier if you want to draw the elements with larger
* [elevation] after all the elements with a smaller one.
*
+ * Note that this parameter is only supported on Android 9 (Pie) and above. On older versions,
+ * this property always returns [Color.Black] and setting new values is ignored.
+ *
* Usage of this API renders this composable into a separate graphics layer
* @see graphicsLayer
*
@@ -92,6 +95,8 @@
* @param elevation The elevation for the shadow in pixels
* @param shape Defines a shape of the physical object
* @param clip When active, the content drawing clips to the shape.
+ * @param ambientColor Color of the ambient shadow drawn when [elevation] > 0f
+ * @param spotColor Color of the spot shadow that is drawn when [elevation] > 0f
*/
@Stable
fun Modifier.shadow(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
index c3995f3..b678844 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
@@ -285,7 +286,7 @@
* @param trimPathStart specifies the fraction of the path to trim from the start in the
* range from 0 to 1. Values outside the range will wrap around the length of the path.
* Default is 0.
- * @param trimPathStart specifies the fraction of the path to trim from the end in the
+ * @param trimPathEnd specifies the fraction of the path to trim from the end in the
* range from 0 to 1. Values outside the range will wrap around the length of the path.
* Default is 1.
* @param trimPathOffset specifies the fraction to shift the path trim region in the range
@@ -701,6 +702,8 @@
* @param strokeLineCap specifies the linecap for a stroked path
* @param strokeLineJoin specifies the linejoin for a stroked path
* @param strokeLineMiter specifies the miter limit for a stroked path
+ * @param pathFillType specifies the winding rule that decides how the interior of a [Path] is
+ * calculated.
* @param pathBuilder [PathBuilder] lambda for adding [PathNode]s to this path.
*/
inline fun ImageVector.Builder.path(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
index c381de4..3071bfc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
@@ -112,6 +112,8 @@
* @param [name] optional identifier used to identify the root of this vector graphic
* @param [tintColor] optional color used to tint the root group of this vector graphic
* @param [tintBlendMode] BlendMode used in combination with [tintColor]
+ * @param [autoMirror] Determines if the contents of the Vector should be mirrored for right to left
+ * layouts.
* @param [content] Composable used to define the structure and contents of the vector graphic
*/
@Composable
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index f442482..d84b8d3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -17,6 +17,9 @@
package androidx.compose.ui.input.pointer
import androidx.collection.LongSparseArray
+import androidx.collection.MutableLongObjectMap
+import androidx.collection.MutableObjectList
+import androidx.collection.mutableObjectListOf
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -42,8 +45,7 @@
/*@VisibleForTesting*/
internal val root: NodeParent = NodeParent()
- // Only used when removing duplicate Nodes from the Node tree ([removeDuplicateNode]).
- private val vectorForHandlingDuplicateNodes: MutableVector<NodeParent> = mutableVectorOf()
+ private val hitPointerIdsAndNodes = MutableLongObjectMap<MutableObjectList<Node>>(10)
/**
* Associates a [pointerId] to a list of hit [pointerInputNodes] and keeps track of them.
@@ -56,21 +58,34 @@
* @param pointerId The id of the pointer that was hit tested against [PointerInputFilter]s
* @param pointerInputNodes The [PointerInputFilter]s that were hit by [pointerId]. Must be
* ordered from ancestor to descendant.
+ * @param prunePointerIdsAndChangesNotInNodesList Prune [PointerId]s (and associated changes)
+ * that are NOT in the pointerInputNodes parameter from the cached tree of ParentNode/Node.
*/
- fun addHitPath(pointerId: PointerId, pointerInputNodes: List<Modifier.Node>) {
+ fun addHitPath(
+ pointerId: PointerId,
+ pointerInputNodes: List<Modifier.Node>,
+ prunePointerIdsAndChangesNotInNodesList: Boolean = false
+ ) {
var parent: NodeParent = root
+ hitPointerIdsAndNodes.clear()
var merging = true
- var nodeBranchPathToSkipDuringDuplicateNodeRemoval: Node? = null
eachPin@ for (i in pointerInputNodes.indices) {
val pointerInputNode = pointerInputNodes[i]
+
if (merging) {
val node = parent.children.firstOrNull {
it.modifierNode == pointerInputNode
}
+
if (node != null) {
node.markIsIn()
node.pointerIds.add(pointerId)
+
+ val mutableObjectList =
+ hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
+
+ mutableObjectList.add(node)
parent = node
continue@eachPin
} else {
@@ -82,52 +97,30 @@
pointerIds.add(pointerId)
}
- if (nodeBranchPathToSkipDuringDuplicateNodeRemoval == null) {
- // Null means this is the first new Node created that will need a new branch path
- // (possibly from a pre-existing cached version of the node chain).
- // If that is the case, we need to skip this path when looking for duplicate
- // nodes to remove (that may have previously existed somewhere else in the tree).
- nodeBranchPathToSkipDuringDuplicateNodeRemoval = node
- } else {
- // Every node after the top new node (that is, the top Node in the new path)
- // could have potentially existed somewhere else in the cached node tree, and
- // we need to remove it if we are adding it to this new branch.
- removeDuplicateNode(node, nodeBranchPathToSkipDuringDuplicateNodeRemoval)
- }
+ val mutableObjectList =
+ hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
+
+ mutableObjectList.add(node)
parent.children.add(node)
parent = node
}
- }
- /*
- * Removes duplicate nodes when using a cached version of the node tree. Uses breadth-first
- * search for simplicity (and because the tree will be very small).
- */
- private fun removeDuplicateNode(
- duplicateNodeToRemove: Node,
- headOfPathToSkip: Node
- ) {
- vectorForHandlingDuplicateNodes.clear()
- vectorForHandlingDuplicateNodes.add(root)
-
- while (vectorForHandlingDuplicateNodes.isNotEmpty()) {
- val parent = vectorForHandlingDuplicateNodes.removeAt(0)
-
- for (index in parent.children.indices) {
- val child = parent.children[index]
- if (child == headOfPathToSkip) continue
- if (child.modifierNode == duplicateNodeToRemove.modifierNode) {
- // Assumes there is only one unique Node in the tree (not copies).
- // This also removes all children attached below the node.
- parent.children.remove(child)
- return
- }
- vectorForHandlingDuplicateNodes.add(child)
+ if (prunePointerIdsAndChangesNotInNodesList) {
+ hitPointerIdsAndNodes.forEach { key, value ->
+ removeInvalidPointerIdsAndChanges(key, value)
}
}
}
+ // Removes pointers/changes that are not in the latest hit test
+ private fun removeInvalidPointerIdsAndChanges(
+ pointerId: Long,
+ hitNodes: MutableObjectList<Node>
+ ) {
+ root.removeInvalidPointerIdsAndChanges(pointerId, hitNodes)
+ }
+
/**
* Dispatches [internalPointerEvent] through the hierarchy.
*
@@ -175,13 +168,13 @@
}
/**
- * Removes [PointerInputFilter]s that have been removed from the component tree.
+ * Removes detached Pointer Input Modifier Nodes.
*/
// TODO(shepshapard): Ideally, we can process the detaching of PointerInputFilters at the time
// that either their associated LayoutNode is removed from the three, or their
// associated PointerInputModifier is removed from a LayoutNode.
- fun removeDetachedPointerInputFilters() {
- root.removeDetachedPointerInputFilters()
+ fun removeDetachedPointerInputNodes() {
+ root.removeDetachedPointerInputModifierNodes()
}
}
@@ -272,19 +265,29 @@
children.clear()
}
+ open fun removeInvalidPointerIdsAndChanges(
+ pointerIdValue: Long,
+ hitNodes: MutableObjectList<Node>
+ ) {
+ children.forEach {
+ it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes)
+ }
+ }
+
/**
* Removes all child [Node]s that are no longer attached to the compose tree.
*/
- fun removeDetachedPointerInputFilters() {
+ fun removeDetachedPointerInputModifierNodes() {
var index = 0
while (index < children.size) {
val child = children[index]
+
if (!child.modifierNode.isAttached) {
- children.removeAt(index)
child.dispatchCancel()
+ children.removeAt(index)
} else {
index++
- child.removeDetachedPointerInputFilters()
+ child.removeDetachedPointerInputModifierNodes()
}
}
}
@@ -327,6 +330,22 @@
private var isIn = true
private var hasExited = true
+ override fun removeInvalidPointerIdsAndChanges(
+ pointerIdValue: Long,
+ hitNodes: MutableObjectList<Node>
+ ) {
+ if (this.pointerIds.contains(pointerIdValue)) {
+ if (!hitNodes.contains(this)) {
+ this.pointerIds.remove(pointerIdValue)
+ this.relevantChanges.remove(pointerIdValue)
+ }
+ }
+
+ children.forEach {
+ it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes)
+ }
+ }
+
override fun dispatchMainEventPass(
changes: LongSparseArray<PointerInputChange>,
parentCoordinates: LayoutCoordinates,
@@ -342,6 +361,7 @@
return dispatchIfNeeded {
val event = pointerEvent!!
val size = coordinates!!.size
+
// Dispatch on the tunneling pass.
modifierNode.dispatchForKind(Nodes.PointerInput) {
it.onPointerEvent(event, PointerEventPass.Initial, size)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
index f2b624c..61e45fa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
@@ -168,24 +168,37 @@
if (pass == Main) {
// Cursor within the surface area of this node's bounds
if (pointerEvent.type == PointerEventType.Enter) {
- cursorInBoundsOfNode = true
- displayIconIfDescendantsDoNotHavePriority()
+ onEnter()
} else if (pointerEvent.type == PointerEventType.Exit) {
- cursorInBoundsOfNode = false
+ onExit()
+ }
+ }
+ }
+
+ private fun onEnter() {
+ cursorInBoundsOfNode = true
+ displayIconIfDescendantsDoNotHavePriority()
+ }
+
+ private fun onExit() {
+ if (cursorInBoundsOfNode) {
+ cursorInBoundsOfNode = false
+
+ if (isAttached) {
displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
}
}
}
override fun onCancelPointerInput() {
- // We aren't processing the event (only listening for enter/exit), so we don't need to
- // do anything.
+ // While pointer icon only really cares about enter/exit, there are some cases (dynamically
+ // adding Modifier Nodes) where a modifier might be cancelled but hasn't been detached or
+ // exited, so we need to cover that case.
+ onExit()
}
override fun onDetach() {
- cursorInBoundsOfNode = false
- displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
-
+ onExit()
super.onDetach()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index 4230b9c..e514e78 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -98,15 +98,22 @@
val isTouchEvent = pointerInputChange.type == PointerType.Touch
root.hitTest(pointerInputChange.position, hitResult, isTouchEvent)
if (hitResult.isNotEmpty()) {
- hitPathTracker.addHitPath(pointerInputChange.id, hitResult)
+ hitPathTracker.addHitPath(
+ pointerId = pointerInputChange.id,
+ pointerInputNodes = hitResult,
+ // Prunes PointerIds (and changes) to support dynamically
+ // adding/removing pointer input modifier nodes.
+ // Note: We do not do this for hover because hover relies on those
+ // non hit PointerIds to trigger hover exit events.
+ prunePointerIdsAndChangesNotInNodesList =
+ pointerInputChange.changedToDownIgnoreConsumed()
+ )
hitResult.clear()
}
}
}
- // Remove [PointerInputFilter]s that are no longer valid and refresh the offset information
- // for those that are.
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
// Dispatch to PointerInputFilters
val dispatchedToSomething =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 91c2d87..45c7b33 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -190,11 +190,21 @@
internal inline fun DelegatableNode.visitLocalDescendants(
mask: Int,
block: (Modifier.Node) -> Unit
+) = visitLocalDescendants(
+ mask = mask,
+ includeSelf = false,
+ block = block
+)
+
+internal inline fun DelegatableNode.visitLocalDescendants(
+ mask: Int,
+ includeSelf: Boolean = false,
+ block: (Modifier.Node) -> Unit
) {
checkPrecondition(node.isAttached) { "visitLocalDescendants called on an unattached node" }
val self = node
if (self.aggregateChildKindSet and mask == 0) return
- var next = self.child
+ var next = if (includeSelf) self else self.child
while (next != null) {
if (next.kindSet and mask != 0) {
block(next)
@@ -217,6 +227,13 @@
}
}
+internal inline fun <reified T> DelegatableNode.visitSelfAndLocalDescendants(
+ type: NodeKind<T>,
+ block: (T) -> Unit
+) = visitLocalDescendants(mask = type.mask, includeSelf = true) {
+ it.dispatchForKind(type, block)
+}
+
internal inline fun <reified T> DelegatableNode.visitLocalDescendants(
type: NodeKind<T>,
block: (T) -> Unit
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
index c41e989..0203f66 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
@@ -40,6 +40,8 @@
override var size: Int = 0
private set
+ var shouldSharePointerInputWithSibling = true
+
/**
* `true` when there has been a direct hit within touch bounds ([hit] called) or
* `false` otherwise.
@@ -95,6 +97,9 @@
*/
fun hit(node: Modifier.Node, isInLayer: Boolean, childHitTest: () -> Unit) {
hitInMinimumTouchTarget(node, -1f, isInLayer, childHitTest)
+ if (node.coordinator?.shouldSharePointerInputWithSiblings() == false) {
+ shouldSharePointerInputWithSibling = false
+ }
}
/**
@@ -238,6 +243,7 @@
fun clear() {
hitDepth = -1
resizeToHitDepth()
+ shouldSharePointerInputWithSibling = true
}
private inner class HitTestResultIterator(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
index 26d852c..3b79baa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
@@ -240,9 +240,7 @@
val continueHitTest: Boolean
if (!wasHit) {
continueHitTest = true
- } else if (
- child.outerCoordinator.shouldSharePointerInputWithSiblings()
- ) {
+ } else if (hitTestResult.shouldSharePointerInputWithSibling) {
hitTestResult.acceptHits()
continueHitTest = true
} else {
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 8a6b586..c3aa5ec 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
@@ -1215,7 +1215,10 @@
val start = headNode(Nodes.PointerInput.includeSelfInTraversal) ?: return false
if (start.isAttached) {
- start.visitLocalDescendants(Nodes.PointerInput) {
+ // We have to check both the self and local descendants, because the `start` can also
+ // be a `PointerInputModifierNode` (when the first modifier node on the LayoutNode is
+ // a `PointerInputModifierNode`).
+ start.visitSelfAndLocalDescendants(Nodes.PointerInput) {
if (it.sharePointerInputWithSiblings()) return true
}
}
diff --git a/core/core/src/main/java/androidx/core/text/HtmlCompat.java b/core/core/src/main/java/androidx/core/text/HtmlCompat.java
index 7ae8f24..61ec500 100644
--- a/core/core/src/main/java/androidx/core/text/HtmlCompat.java
+++ b/core/core/src/main/java/androidx/core/text/HtmlCompat.java
@@ -46,49 +46,49 @@
public final class HtmlCompat {
/**
* Option for {@link #fromHtml(String, int)}: Wrap consecutive lines of text delimited by '\n'
- * inside <p> elements. {@link BulletSpan}s are ignored.
+ * inside <code><p></code> elements. {@link BulletSpan}s are ignored.
*/
public static final int TO_HTML_PARAGRAPH_LINES_CONSECUTIVE =
Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE;
/**
* Option for {@link #fromHtml(String, int)}: Wrap each line of text delimited by '\n' inside a
- * <p> or a <li> element. This allows {@link ParagraphStyle}s attached to be
- * encoded as CSS styles within the corresponding <p> or <li> element.
+ * <code><p></code> or a <code><li></code> element. This allows {@link ParagraphStyle}s attached to be
+ * encoded as CSS styles within the corresponding <code><p></code> or <code><li></code> element.
*/
public static final int TO_HTML_PARAGRAPH_LINES_INDIVIDUAL =
Html.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL;
/**
- * Flag indicating that texts inside <p> elements will be separated from other texts with
+ * Flag indicating that texts inside <code><p></code> elements will be separated from other texts with
* one newline character by default.
*/
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH =
Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH;
/**
- * Flag indicating that texts inside <h1>~<h6> elements will be separated from
+ * Flag indicating that texts inside <code><h1></code>~<code><h6></code> elements will be separated from
* other texts with one newline character by default.
*/
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_HEADING =
Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING;
/**
- * Flag indicating that texts inside <li> elements will be separated from other texts
+ * Flag indicating that texts inside <code><li></code> elements will be separated from other texts
* with one newline character by default.
*/
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM =
Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM;
/**
- * Flag indicating that texts inside <ul> elements will be separated from other texts
+ * Flag indicating that texts inside <code><ul></code> elements will be separated from other texts
* with one newline character by default.
*/
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST =
Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST;
/**
- * Flag indicating that texts inside <div> elements will be separated from other texts
+ * Flag indicating that texts inside <code><div><c/ode> elements will be separated from other texts
* with one newline character by default.
*/
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_DIV =
Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV;
/**
- * Flag indicating that texts inside <blockquote> elements will be separated from other
+ * Flag indicating that texts inside <code><blockquote></code> elements will be separated from other
* texts with one newline character by default.
*/
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE =
diff --git a/datastore/datastore-compose-samples/build.gradle b/datastore/datastore-compose-samples/build.gradle
index 698b179..2e5f736 100644
--- a/datastore/datastore-compose-samples/build.gradle
+++ b/datastore/datastore-compose-samples/build.gradle
@@ -33,9 +33,10 @@
}
dependencies {
+ compileOnly(projectOrArtifact(":datastore:datastore-preferences-external-protobuf"))
+
implementation(libs.protobufLite)
implementation(libs.kotlinStdlib)
-
implementation('androidx.core:core-ktx:1.7.0')
implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.3.1')
implementation('androidx.activity:activity-compose:1.3.1')
diff --git a/datastore/datastore-preferences-core/build.gradle b/datastore/datastore-preferences-core/build.gradle
index cff152d..8dacda8 100644
--- a/datastore/datastore-preferences-core/build.gradle
+++ b/datastore/datastore-preferences-core/build.gradle
@@ -21,7 +21,6 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.BundleInsideHelper
import androidx.build.LibraryType
import androidx.build.PlatformIdentifier
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
@@ -67,6 +66,9 @@
}
jvmMain {
dependsOn(commonMain)
+ dependencies {
+ implementation(project(":datastore:datastore-preferences-proto"))
+ }
}
jvmTest {
dependsOn(commonTest)
@@ -107,19 +109,6 @@
}
}
-BundleInsideHelper.forInsideJarKmp(
- project,
- /* from = */ "com.google.protobuf",
- /* to = */ "androidx.datastore.preferences.protobuf",
- // proto-lite dependency includes .proto files, which are not used and would clash if
- // users also use proto library directly
- /* dropResourcesWithSuffix = */ ".proto"
-)
-
-dependencies {
- bundleInside(project(":datastore:datastore-preferences-proto"))
-}
-
androidx {
name = "Preferences DataStore Core"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/datastore/datastore-preferences-external-protobuf/api/current.txt b/datastore/datastore-preferences-external-protobuf/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/datastore/datastore-preferences-external-protobuf/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/datastore/datastore-preferences-external-protobuf/api/restricted_current.txt b/datastore/datastore-preferences-external-protobuf/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/datastore/datastore-preferences-external-protobuf/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/datastore/datastore-preferences-external-protobuf/build.gradle b/datastore/datastore-preferences-external-protobuf/build.gradle
new file mode 100644
index 0000000..506da76
--- /dev/null
+++ b/datastore/datastore-preferences-external-protobuf/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXRepackagePlugin")
+ id("java-library")
+}
+
+repackage {
+ addRelocation {
+ sourcePackage = "com.google.protobuf"
+ targetPackage = "androidx.datastore.preferences.protobuf"
+ }
+ artifactId = "datastore-preferences-external-protobuf"
+}
+
+dependencies {
+ repackage(libs.protobufLite)
+}
+
+androidx {
+ name = "Preferences External Protobuf"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "Repackaged proto-lite dependency for use by datastore preferences"
+ doNotDocumentReason = "Repackaging only"
+ license.name = "BSD-3-Clause"
+ license.url = "https://opensource.org/licenses/BSD-3-Clause"
+}
diff --git a/datastore/datastore-preferences-proto/build.gradle b/datastore/datastore-preferences-proto/build.gradle
index d5b5bb1..860174f 100644
--- a/datastore/datastore-preferences-proto/build.gradle
+++ b/datastore/datastore-preferences-proto/build.gradle
@@ -21,17 +21,30 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import androidx.build.LibraryType
+import androidx.build.RunApiTasks
plugins {
id("AndroidXPlugin")
+ id("AndroidXRepackagePlugin")
id("kotlin")
id("com.google.protobuf")
}
+repackage {
+ // Must match what is in datastore/datastore-preferences-external-protobuf/build.gradle
+ addRelocation {
+ sourcePackage = "com.google.protobuf"
+ targetPackage = "androidx.datastore.preferences.protobuf"
+ }
+}
+
dependencies {
- implementation(libs.protobufLite)
+ api(project(":datastore:datastore-preferences-external-protobuf"))
+ // Must be compileOnly to not bring in protobufLite in runtime
+ // Repackaged protobufLite brought in by
+ // project(":datastore:datastore-preferences-external-protobuf") and used at runtime
+ compileOnly(libs.protobufLite)
compileOnly(project(":datastore:datastore-core"))
}
@@ -61,8 +74,9 @@
androidx {
name = "Preferences DataStore Proto"
- publish = Publish.NONE
+ type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2020"
- description = "Jarjar the generated proto and proto-lite dependency for use by " +
- "datastore-preferences."
+ description = "Jarjar the generated proto for use by datastore-preferences."
+ runApiTasks = new RunApiTasks.No("Metalava doesn't properly parse the proto sources " +
+ "(b/180579063)")
}
diff --git a/development/update_studio.sh b/development/update_studio.sh
index ef74a284..324322f 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -7,8 +7,8 @@
# Versions that the user should update when running this script
echo Getting Studio version and link
-AGP_VERSION=${1:-8.4.0-alpha12}
-STUDIO_VERSION_STRING=${2:-"Android Studio Jellyfish | 2023.3.1 Canary 12"}
+AGP_VERSION=${1:-8.5.0-alpha06}
+STUDIO_VERSION_STRING=${2:-"Android Studio Koala | 2024.1.1 Canary 6"}
# Get studio version number from version name
STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep "<iframe " | sed "s/.* src=\"\([^\"]*\)\".*/\1/g"`
diff --git a/gradle.properties b/gradle.properties
index 7a42aa7..68fc0ca 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,6 +21,7 @@
# fullsdk-linux/**/package.xml -> b/291331139
org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=**/prebuilts/fullsdk-linux;**/prebuilts/fullsdk-linux/platforms/android-*/package.xml
+android.javaCompile.suppressSourceTargetDeprecationWarning=true
android.lint.baselineOmitLineNumbers=true
android.lint.printStackTrace=true
android.builder.sdkDownload=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9c01869..da3f63c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,13 +2,13 @@
# -----------------------------------------------------------------------------
# All of the following should be updated in sync.
# -----------------------------------------------------------------------------
-androidGradlePlugin = "8.4.0-alpha12"
+androidGradlePlugin = "8.5.0-alpha06"
# NOTE: When updating the lint version we also need to update the `api` version
# supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "31.4.0-alpha12"
+androidLint = "31.5.0-alpha06"
# Once you have a chosen version of AGP to upgrade to, go to
# https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2023.3.1.12"
+androidStudio = "2024.1.1.4"
# -----------------------------------------------------------------------------
androidGradlePluginMin = "7.0.4"
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e5c45a0..1d107cd 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=../../../../tools/external/gradle/gradle-8.7-bin.zip
-distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
+distributionUrl=../../../../tools/external/gradle/gradle-8.8-rc-1-bin.zip
+distributionSha256Sum=a2e1cfee7ffdeee86015b85b2dd2a435032c40eedc01d8172285556c7d8fea13
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index e13d44f..dce5e38 100755
--- a/gradlew
+++ b/gradlew
@@ -16,7 +16,6 @@
OUT_DIR="$(cd $OUT_DIR && pwd -P)"
export GRADLE_USER_HOME="$OUT_DIR/.gradle"
export TMPDIR="$OUT_DIR/tmp"
- mkdir -p "$TMPDIR"
else
CHECKOUT_ROOT="$(cd $SCRIPT_PATH/../.. && pwd -P)"
export OUT_DIR="$CHECKOUT_ROOT/out"
@@ -230,9 +229,6 @@
if [ "$GRADLE_USER_HOME" != "" ]; then
HOME_SYSTEM_PROPERTY_ARGUMENT="-Duser.home=$GRADLE_USER_HOME"
fi
-if [ "$TMPDIR" != "" ]; then
- TMPDIR_ARG="-Djava.io.tmpdir=$TMPDIR"
-fi
if [[ " ${@} " =~ " --clean " ]]; then
cleanCaches=true
@@ -407,6 +403,11 @@
}
function runGradle() {
+ if [ "$TMPDIR" != "" ]; then
+ mkdir -p "$TMPDIR"
+ TMPDIR_ARG="-Djava.io.tmpdir=$TMPDIR"
+ fi
+
processOutput=false
if [[ " ${@} " =~ " -Pandroidx.validateNoUnrecognizedMessages " ]]; then
processOutput=true
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
index 97d6f65..469140c 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
@@ -111,6 +111,7 @@
* See https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglMakeCurrent.xhtml for more
* information
*
+ * @param context EGL rendering context to be attached to the surfaces
* @param drawSurface EGLSurface to draw pixels into.
* @param readSurface EGLSurface used for read/copy operations.
*/
diff --git a/kruth/kruth/api/current.ignore b/kruth/kruth/api/current.ignore
index 2fc7dd5..5589f1d 100644
--- a/kruth/kruth/api/current.ignore
+++ b/kruth/kruth/api/current.ignore
@@ -73,8 +73,6 @@
Removed class androidx.kruth.PrimitiveDoubleArraySubject.DoubleArrayAsIterable
RemovedClass: androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable:
Removed class androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable
-RemovedClass: androidx.kruth.TableSubject:
- Removed class androidx.kruth.TableSubject
RemovedClass: androidx.kruth.Truth:
Removed class androidx.kruth.Truth
RemovedClass: androidx.kruth.TruthJUnit:
diff --git a/kruth/kruth/api/current.txt b/kruth/kruth/api/current.txt
index 857e856..26a1b93 100644
--- a/kruth/kruth/api/current.txt
+++ b/kruth/kruth/api/current.txt
@@ -201,6 +201,7 @@
method public static <T> androidx.kruth.GuavaOptionalSubject<T> assertThat(com.google.common.base.Optional<T>? actual);
method public static <K, V> androidx.kruth.MultimapSubject<K,V> assertThat(com.google.common.collect.Multimap<K,V> actual);
method public static <T> androidx.kruth.MultisetSubject<T> assertThat(com.google.common.collect.Multiset<T> actual);
+ method public static <R, C, V> androidx.kruth.TableSubject<R,C,V> assertThat(com.google.common.collect.Table<R,C,V> actual);
method public static androidx.kruth.ClassSubject assertThat(Class<?> actual);
method public static androidx.kruth.BigDecimalSubject assertThat(java.math.BigDecimal actual);
}
@@ -418,6 +419,21 @@
method public SubjectT createSubject(androidx.kruth.FailureMetadata metadata, ActualT? actual);
}
+ public final class TableSubject<R, C, V> extends androidx.kruth.Subject<com.google.common.collect.Table<R,C,V>> {
+ method public void contains(R rowKey, C columnKey);
+ method public void containsCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+ method public void containsCell(R rowKey, C colKey, V value);
+ method public void containsColumn(C columnKey);
+ method public void containsRow(R rowKey);
+ method public void containsValue(V value);
+ method public void doesNotContain(R rowKey, C columnKey);
+ method public void doesNotContainCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+ method public void doesNotContainCell(R rowKey, C colKey, V value);
+ method public void hasSize(int expectedSize);
+ method public void isEmpty();
+ method public void isNotEmpty();
+ }
+
public class ThrowableSubject<T extends java.lang.Throwable> extends androidx.kruth.Subject<T> {
ctor protected ThrowableSubject(androidx.kruth.FailureMetadata metadata, T? actual);
method public final androidx.kruth.ThrowableSubject<java.lang.Throwable> hasCauseThat();
diff --git a/kruth/kruth/api/restricted_current.ignore b/kruth/kruth/api/restricted_current.ignore
index 2fc7dd5..5589f1d 100644
--- a/kruth/kruth/api/restricted_current.ignore
+++ b/kruth/kruth/api/restricted_current.ignore
@@ -73,8 +73,6 @@
Removed class androidx.kruth.PrimitiveDoubleArraySubject.DoubleArrayAsIterable
RemovedClass: androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable:
Removed class androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable
-RemovedClass: androidx.kruth.TableSubject:
- Removed class androidx.kruth.TableSubject
RemovedClass: androidx.kruth.Truth:
Removed class androidx.kruth.Truth
RemovedClass: androidx.kruth.TruthJUnit:
diff --git a/kruth/kruth/api/restricted_current.txt b/kruth/kruth/api/restricted_current.txt
index 90ec259..4d8c416 100644
--- a/kruth/kruth/api/restricted_current.txt
+++ b/kruth/kruth/api/restricted_current.txt
@@ -201,6 +201,7 @@
method public static <T> androidx.kruth.GuavaOptionalSubject<T> assertThat(com.google.common.base.Optional<T>? actual);
method public static <K, V> androidx.kruth.MultimapSubject<K,V> assertThat(com.google.common.collect.Multimap<K,V> actual);
method public static <T> androidx.kruth.MultisetSubject<T> assertThat(com.google.common.collect.Multiset<T> actual);
+ method public static <R, C, V> androidx.kruth.TableSubject<R,C,V> assertThat(com.google.common.collect.Table<R,C,V> actual);
method public static androidx.kruth.ClassSubject assertThat(Class<?> actual);
method public static androidx.kruth.BigDecimalSubject assertThat(java.math.BigDecimal actual);
}
@@ -419,6 +420,21 @@
method public SubjectT createSubject(androidx.kruth.FailureMetadata metadata, ActualT? actual);
}
+ public final class TableSubject<R, C, V> extends androidx.kruth.Subject<com.google.common.collect.Table<R,C,V>> {
+ method public void contains(R rowKey, C columnKey);
+ method public void containsCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+ method public void containsCell(R rowKey, C colKey, V value);
+ method public void containsColumn(C columnKey);
+ method public void containsRow(R rowKey);
+ method public void containsValue(V value);
+ method public void doesNotContain(R rowKey, C columnKey);
+ method public void doesNotContainCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+ method public void doesNotContainCell(R rowKey, C colKey, V value);
+ method public void hasSize(int expectedSize);
+ method public void isEmpty();
+ method public void isNotEmpty();
+ }
+
public class ThrowableSubject<T extends java.lang.Throwable> extends androidx.kruth.Subject<T> {
ctor protected ThrowableSubject(androidx.kruth.FailureMetadata metadata, T? actual);
method public final androidx.kruth.ThrowableSubject<java.lang.Throwable> hasCauseThat();
diff --git a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt
index ea1fbc7..9b8c123 100644
--- a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt
+++ b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt
@@ -19,6 +19,7 @@
import com.google.common.base.Optional
import com.google.common.collect.Multimap
import com.google.common.collect.Multiset
+import com.google.common.collect.Table
import java.math.BigDecimal
fun assertThat(actual: Class<*>): ClassSubject =
@@ -35,3 +36,6 @@
fun <K, V> assertThat(actual: Multimap<K, V>): MultimapSubject<K, V> =
MultimapSubject(actual = actual)
+
+fun <R, C, V> assertThat(actual: Table<R, C, V>): TableSubject<R, C, V> =
+ TableSubject(actual = actual)
diff --git a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt
index 891e0a2..e4030e6 100644
--- a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt
+++ b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt
@@ -19,6 +19,7 @@
import com.google.common.base.Optional
import com.google.common.collect.Multimap
import com.google.common.collect.Multiset
+import com.google.common.collect.Table
import java.math.BigDecimal
internal actual interface PlatformStandardSubjectBuilder {
@@ -28,6 +29,7 @@
fun that(actual: BigDecimal): BigDecimalSubject
fun <T> that(actual: Multiset<T>): MultisetSubject<T>
fun <K, V> that(actual: Multimap<K, V>): MultimapSubject<K, V>
+ fun <R, C, V> that(actual: Table<R, C, V>): TableSubject<R, C, V>
}
internal actual class PlatformStandardSubjectBuilderImpl actual constructor(
@@ -48,4 +50,7 @@
override fun <K, V> that(actual: Multimap<K, V>): MultimapSubject<K, V> =
MultimapSubject(actual = actual, metadata = metadata)
+
+ override fun <R, C, V> that(actual: Table<R, C, V>): TableSubject<R, C, V> =
+ TableSubject(actual = actual, metadata = metadata)
}
diff --git a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/TableSubject.jvm.kt b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/TableSubject.jvm.kt
new file mode 100644
index 0000000..80f873e
--- /dev/null
+++ b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/TableSubject.jvm.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024 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.
+ */
+
+package androidx.kruth
+
+import androidx.kruth.Fact.Companion.fact
+import androidx.kruth.Fact.Companion.simpleFact
+import com.google.common.collect.Table
+import com.google.common.collect.Table.Cell
+import com.google.common.collect.Tables.immutableCell
+
+class TableSubject<R, C, V> internal constructor(
+ actual: Table<R, C, V>,
+ metadata: FailureMetadata = FailureMetadata(),
+) : Subject<Table<R, C, V>>(actual, metadata, typeDescriptionOverride = null) {
+
+ /** Fails if the table is not empty. */
+ fun isEmpty() {
+ requireNonNull(actual)
+
+ if (!actual.isEmpty) {
+ failWithActual(simpleFact("expected to be empty"))
+ }
+ }
+
+ /** Fails if the table is empty. */
+ fun isNotEmpty() {
+ requireNonNull(actual)
+
+ if (actual.isEmpty) {
+ failWithoutActual(simpleFact("expected not to be empty"))
+ }
+ }
+
+ /** Fails if the table does not have the given size. */
+ fun hasSize(expectedSize: Int) {
+ require(expectedSize >= 0) { "expectedSize($expectedSize) must be >= 0" }
+ requireNonNull(actual)
+
+ check("size()").that(actual.size()).isEqualTo(expectedSize)
+ }
+
+ /** Fails if the table does not contain a mapping for the given row key and column key. */
+ fun contains(rowKey: R, columnKey: C) {
+ requireNonNull(actual)
+
+ if (!actual.contains(rowKey, columnKey)) {
+ failWithActual(
+ simpleFact("expected to contain mapping for row-column key pair"),
+ fact("row key", rowKey),
+ fact("column key", columnKey),
+ )
+ }
+ }
+
+ /** Fails if the table contains a mapping for the given row key and column key. */
+ fun doesNotContain(rowKey: R, columnKey: C) {
+ requireNonNull(actual)
+
+ if (actual.contains(rowKey, columnKey)) {
+ failWithoutActual(
+ simpleFact("expected not to contain mapping for row-column key pair"),
+ fact("row key", rowKey),
+ fact("column key", columnKey),
+ fact("but contained value", actual[rowKey, columnKey]),
+ fact("full contents", actual),
+ )
+ }
+ }
+
+ /** Fails if the table does not contain the given cell. */
+ fun containsCell(rowKey: R, colKey: C, value: V) {
+ containsCell(immutableCell(rowKey, colKey, value))
+ }
+
+ /** Fails if the table does not contain the given cell. */
+ fun containsCell(cell: Cell<R, C, V>?) {
+ requireNonNull(cell)
+ requireNonNull(actual)
+
+ checkNoNeedToDisplayBothValues("cellSet()")
+ .that(actual.cellSet())
+ .contains(cell)
+ }
+
+ /** Fails if the table contains the given cell. */
+ fun doesNotContainCell(rowKey: R, colKey: C, value: V) {
+ doesNotContainCell(immutableCell(rowKey, colKey, value))
+ }
+
+ /** Fails if the table contains the given cell. */
+ fun doesNotContainCell(cell: Cell<R, C, V>?) {
+ requireNonNull(cell)
+ requireNonNull(actual)
+
+ checkNoNeedToDisplayBothValues("cellSet()")
+ .that(actual.cellSet())
+ .doesNotContain(cell)
+ }
+
+ /** Fails if the table does not contain the given row key. */
+ fun containsRow(rowKey: R) {
+ requireNonNull(actual)
+
+ check("rowKeySet()").that(actual.rowKeySet()).contains(rowKey)
+ }
+
+ /** Fails if the table does not contain the given column key. */
+ fun containsColumn(columnKey: C) {
+ requireNonNull(actual)
+
+ check("columnKeySet()").that(actual.columnKeySet()).contains(columnKey)
+ }
+
+ /** Fails if the table does not contain the given value. */
+ fun containsValue(value: V) {
+ requireNonNull(actual)
+
+ check("values()").that(actual.values()).contains(value)
+ }
+}
diff --git a/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/TableSubjectTest.jvm.kt b/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/TableSubjectTest.jvm.kt
new file mode 100644
index 0000000..a2a1157
--- /dev/null
+++ b/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/TableSubjectTest.jvm.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2024 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.
+ */
+
+package androidx.kruth
+
+import com.google.common.collect.ImmutableTable
+import com.google.common.collect.Tables.immutableCell
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+
+class TableSubjectTest {
+
+ @Test
+ fun tableIsEmpty() {
+ val table = ImmutableTable.of<String, String, String>()
+ assertThat(table).isEmpty()
+ }
+
+ @Test
+ fun tableIsEmptyWithFailure() {
+ val table = ImmutableTable.of(1, 5, 7)
+ assertFailsWith<AssertionError> {
+ assertThat(table).isEmpty()
+ }
+ }
+
+ @Test
+ fun tableIsNotEmpty() {
+ val table = ImmutableTable.of(1, 5, 7)
+ assertThat(table).isNotEmpty()
+ }
+
+ @Test
+ fun tableIsNotEmptyWithFailure() {
+ val table = ImmutableTable.of<Int, Int, Int>()
+ assertFailsWith<AssertionError> {
+ assertThat(table).isNotEmpty()
+ }
+ }
+
+ @Test
+ fun hasSize() {
+ assertThat(ImmutableTable.of(1, 2, 3)).hasSize(1)
+ }
+
+ @Test
+ fun hasSizeZero() {
+ assertThat(ImmutableTable.of<Any, Any, Any>()).hasSize(0)
+ }
+
+ @Test
+ fun hasSizeNegative() {
+ assertFailsWith<IllegalArgumentException> {
+ assertThat(ImmutableTable.of(1, 2, 3)).hasSize(-1)
+ }
+ }
+
+ @Test
+ fun contains() {
+ val table = ImmutableTable.of("row", "col", "val")
+ assertThat(table).contains("row", "col")
+ }
+
+ @Test
+ fun containsFailure() {
+ val table = ImmutableTable.of("row", "col", "val")
+
+ assertFailsWith<AssertionError> {
+ assertThat(table).contains("row", "otherCol")
+ }
+ }
+
+ @Test
+ fun doesNotContain() {
+ val table = ImmutableTable.of("row", "col", "val")
+ assertThat(table).doesNotContain("row", "row")
+ assertThat(table).doesNotContain("col", "row")
+ assertThat(table).doesNotContain("col", "col")
+ assertThat(table).doesNotContain(null, null)
+ }
+
+ @Test
+ fun doesNotContainFailure() {
+ val table = ImmutableTable.of("row", "col", "val")
+ assertFailsWith<AssertionError> {
+ assertThat(table).doesNotContain("row", "col")
+ }
+ }
+
+ @Test
+ fun containsCell() {
+ val table = ImmutableTable.of("row", "col", "val")
+ assertThat(table).containsCell("row", "col", "val")
+ assertThat(table).containsCell(immutableCell("row", "col", "val"))
+ }
+
+ @Test
+ fun containsCellFailure() {
+ val table = ImmutableTable.of("row", "col", "val")
+ assertFailsWith<AssertionError> {
+ assertThat(table).containsCell("row", "row", "val")
+ }
+ }
+
+ @Test
+ fun doesNotContainCell() {
+ val table = ImmutableTable.of("row", "col", "val")
+ assertThat(table).doesNotContainCell("row", "row", "val")
+ assertThat(table).doesNotContainCell("col", "row", "val")
+ assertThat(table).doesNotContainCell("col", "col", "val")
+ assertThat(table).doesNotContainCell(null, null, null)
+ assertThat(table).doesNotContainCell(immutableCell("row", "row", "val"))
+ assertThat(table).doesNotContainCell(immutableCell("col", "row", "val"))
+ assertThat(table).doesNotContainCell(immutableCell("col", "col", "val"))
+ assertThat(table).doesNotContainCell(immutableCell(null, null, null))
+ }
+
+ @Test
+ fun doesNotContainCellFailure() {
+ val table = ImmutableTable.of("row", "col", "val")
+ assertFailsWith<AssertionError> {
+ assertThat(table).doesNotContainCell("row", "col", "val")
+ }
+ }
+}
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.android.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.android.kt
index 8bcf5f0..a42b7d8 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.android.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.android.kt
@@ -359,6 +359,76 @@
assertThat(getOptionalCount!!()).isEqualTo(1)
assertThat(savedStateHandle?.keys()).isEqualTo(setOf("count"))
}
+
+ @OptIn(SavedStateHandleSaveableApi::class)
+ @Test
+ fun noConflictKeys_delegate_simpleRestore() {
+ activityTestRuleScenario.scenario.onActivity { activity ->
+ activity.setContent {
+ val viewModel = viewModel<SavingTestViewModel>(activity)
+ val firstClass = FirstClass(viewModel.savedStateHandle)
+ val secondClass = SecondClass(viewModel.savedStateHandle)
+ assertThat(firstClass.savedProperty).isEqualTo("One")
+ assertThat(secondClass.savedProperty).isEqualTo("Two")
+ }
+ }
+
+ activityTestRuleScenario.scenario.recreate()
+
+ activityTestRuleScenario.scenario.onActivity { activity ->
+ activity.setContent {
+ val viewModel = viewModel<SavingTestViewModel>(activity)
+ val firstClass = FirstClass(viewModel.savedStateHandle)
+ val secondClass = SecondClass(viewModel.savedStateHandle)
+ assertThat(firstClass.savedProperty).isEqualTo("One")
+ assertThat(secondClass.savedProperty).isEqualTo("Two")
+ }
+ }
+ }
+
+ @OptIn(SavedStateHandleSaveableApi::class)
+ @Test
+ fun conflictKeys_local_delegate_simpleRestore() {
+
+ fun firstFunction(handle: SavedStateHandle): String {
+ val localProperty by handle.saveable { mutableStateOf("One") }
+ return localProperty
+ }
+
+ fun secondFunction(handle: SavedStateHandle): String {
+ val localProperty by handle.saveable { mutableStateOf("Two") }
+ return localProperty
+ }
+
+ activityTestRuleScenario.scenario.onActivity { activity ->
+ activity.setContent {
+ val savedStateHandle = viewModel<SavingTestViewModel>(activity).savedStateHandle
+ firstFunction(savedStateHandle)
+ secondFunction(savedStateHandle)
+ }
+ }
+
+ activityTestRuleScenario.scenario.recreate()
+
+ activityTestRuleScenario.scenario.onActivity { activity ->
+ activity.setContent {
+ val savedStateHandle = viewModel<SavingTestViewModel>(activity).savedStateHandle
+ // TODO(b/331695354): Fix local property saveable delegate key conflict
+ assertThat(firstFunction(savedStateHandle)).isEqualTo("Two")
+ assertThat(secondFunction(savedStateHandle)).isEqualTo("Two")
+ }
+ }
+ }
}
class SavingTestViewModel(val savedStateHandle: SavedStateHandle) : ViewModel()
+
+class FirstClass(savedStateHandle: SavedStateHandle) {
+ @OptIn(SavedStateHandleSaveableApi::class)
+ val savedProperty by savedStateHandle.saveable { mutableStateOf("One") }
+}
+
+class SecondClass(savedStateHandle: SavedStateHandle) {
+ @OptIn(SavedStateHandleSaveableApi::class)
+ val savedProperty by savedStateHandle.saveable { mutableStateOf("Two") }
+}
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidMain/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.android.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidMain/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.android.kt
index ada4c35..019167e 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidMain/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.android.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidMain/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.android.kt
@@ -117,9 +117,10 @@
saver: Saver<T, out Any> = autoSaver(),
init: () -> T,
): PropertyDelegateProvider<Any?, ReadOnlyProperty<Any?, T>> =
- PropertyDelegateProvider { _, property ->
+ PropertyDelegateProvider { thisRef, property ->
+ val classNamePrefix = if (thisRef != null) thisRef::class.qualifiedName + "." else ""
val value = saveable(
- key = property.name,
+ key = classNamePrefix + property.name,
saver = saver,
init = init
)
@@ -155,9 +156,10 @@
stateSaver: Saver<T, out Any> = autoSaver(),
init: () -> M,
): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> =
- PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> { _, property ->
+ PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> { thisRef, property ->
+ val classNamePrefix = if (thisRef != null) thisRef::class.qualifiedName + "." else ""
val mutableState = saveable(
- key = property.name,
+ key = classNamePrefix + property.name,
stateSaver = stateSaver,
init = init
)
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index 6057cc1..9a36c5f 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -487,7 +487,7 @@
// Builtin R8 desugaring, such as rewriting compare calls (see b/36390874)
if (owner.startsWith("java.") &&
- DesugaredMethodLookup.isDesugared(owner, name, desc, context.sourceSetType)) {
+ DesugaredMethodLookup.isDesugaredMethod(owner, name, desc, context.sourceSetType)) {
return
}
@@ -573,7 +573,7 @@
api: Int
): LintFix? {
val callPsi = call.sourcePsi ?: return null
- if (isKotlin(callPsi)) {
+ if (isKotlin(callPsi.language)) {
// We only support Java right now.
return null
}
diff --git a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
index a9f7fbd..a773acc 100644
--- a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
@@ -59,7 +59,7 @@
return
}
- if (!isKotlin(node)) return
+ if (!isKotlin(node.language)) return
if (!node.isInterface) return
if (node.annotatedWithAnyOf(
// If the interface is not stable, it doesn't need the annotation
diff --git a/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt b/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
index 6ed71ee..bf0227e 100644
--- a/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
@@ -40,7 +40,8 @@
private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
override fun visitAnnotation(node: UAnnotation) {
- if (isJava(node.sourcePsi)) {
+ val element = node.sourcePsi
+ if (element != null && isJava(element.language)) {
checkForAnnotation(node, "NotNull", "NonNull")
checkForAnnotation(node, "Nullable", "Nullable")
}
diff --git a/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt b/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
index 36cbf9cd..9eaf45c 100644
--- a/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
@@ -86,7 +86,9 @@
// here, but that points to impl classes in its hierarchy which leads to
// class loading trouble.
val sourcePsi = element.sourcePsi
- if (isKotlin(sourcePsi) && sourcePsi?.parent?.toString() == "CONSTRUCTOR_CALLEE") {
+ if (sourcePsi != null &&
+ isKotlin(sourcePsi.language) &&
+ sourcePsi.parent?.toString() == "CONSTRUCTOR_CALLEE") {
return
}
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java
index 51fbfe1..19045af 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java
@@ -62,6 +62,7 @@
/**
* Creates a remote playback client for a route.
*
+ * @param context The {@link Context}.
* @param route The media route.
*/
public RemotePlaybackClient(@NonNull Context context, @NonNull MediaRouter.RouteInfo route) {
diff --git a/mediarouter/mediarouter/src/main/res/values-iw/strings.xml b/mediarouter/mediarouter/src/main/res/values-iw/strings.xml
index 03371fa..7bafb1a 100644
--- a/mediarouter/mediarouter/src/main/res/values-iw/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-iw/strings.xml
@@ -18,11 +18,11 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="mr_system_route_name" msgid="7449553026175453403">"מערכת"</string>
<string name="mr_user_route_category_name" msgid="4088331695424166162">"מכשירים"</string>
- <string name="mr_button_content_description" msgid="2939063992730535343">"העברה (cast)"</string>
- <string name="mr_cast_button_disconnected" msgid="8071109333469380363">"העברה (cast). אין חיבור"</string>
- <string name="mr_cast_button_connecting" msgid="6629927151350192407">"העברה (cast). מתבצעת התחברות"</string>
- <string name="mr_cast_button_connected" msgid="6073720094880410356">"העברה (cast). מחובר"</string>
- <string name="mr_chooser_title" msgid="1419936397646839840">"העברה אל"</string>
+ <string name="mr_button_content_description" msgid="2939063992730535343">"הפעלת Cast"</string>
+ <string name="mr_cast_button_disconnected" msgid="8071109333469380363">"הפעלת Cast. אין מכשיר מחובר"</string>
+ <string name="mr_cast_button_connecting" msgid="6629927151350192407">"הפעלת Cast. מתחברים למכשיר"</string>
+ <string name="mr_cast_button_connected" msgid="6073720094880410356">"הפעלת Cast. יש מכשיר מחובר"</string>
+ <string name="mr_chooser_title" msgid="1419936397646839840">"הפעלת Cast אל"</string>
<string name="mr_chooser_searching" msgid="6114250663023140921">"מתבצע חיפוש מכשירים"</string>
<string name="mr_chooser_looking_for_devices" msgid="4257319068277776035">"מתבצע חיפוש מכשירים…"</string>
<string name="mr_controller_disconnect" msgid="7812275474138309497">"ניתוק"</string>
@@ -50,5 +50,5 @@
<string name="mr_chooser_wifi_warning_description_car" msgid="2998902945608081567">"מוודאים שהמכשיר השני והרכב מחוברים לאותה רשת Wi-Fi"</string>
<string name="mr_chooser_wifi_warning_description_unknown" msgid="3459891599800041449">"מוודאים ששני המכשירים מחוברים לאותה רשת Wi-Fi"</string>
<string name="mr_chooser_wifi_learn_more" msgid="3799500840179081429"><a href="https://support.google.com/chromecast/?p=trouble-finding-devices">"מידע נוסף"</a></string>
- <string name="ic_media_route_learn_more_accessibility" msgid="9119039724000326934">"איך להעביר (cast)"</string>
+ <string name="ic_media_route_learn_more_accessibility" msgid="9119039724000326934">"איך להפעיל Cast"</string>
</resources>
diff --git a/playground-common/gradle/wrapper/gradle-wrapper.properties b/playground-common/gradle/wrapper/gradle-wrapper.properties
index 61f9702..fc4b959 100644
--- a/playground-common/gradle/wrapper/gradle-wrapper.properties
+++ b/playground-common/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
-distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-rc-1-bin.zip
+distributionSha256Sum=a2e1cfee7ffdeee86015b85b2dd2a435032c40eedc01d8172285556c7d8fea13
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle b/settings.gradle
index 51022df..84fb587 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -656,6 +656,7 @@
includeProject(":datastore:datastore-compose-samples", [BuildType.COMPOSE])
includeProject(":datastore:datastore-preferences", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
includeProject(":datastore:datastore-preferences-core", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
+includeProject(":datastore:datastore-preferences-external-protobuf", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
includeProject(":datastore:datastore-preferences-proto", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
includeProject(":datastore:datastore-preferences-rxjava2", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
includeProject(":datastore:datastore-preferences-rxjava3", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
diff --git a/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt b/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt
index 2e7601c..4d9ce75 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt
@@ -111,6 +111,10 @@
throw NotImplementedError()
}
+ override fun replace(transformation: Transformer<out Provider<out T>?, in Provider<T>>) {
+ throw NotImplementedError()
+ }
+
@Deprecated("Deprecated in Java")
override fun forUseAtConfigurationTime(): Provider<T> {
throw NotImplementedError()
diff --git a/testutils/testutils-datastore/build.gradle b/testutils/testutils-datastore/build.gradle
index 2721d70..a2057f8 100644
--- a/testutils/testutils-datastore/build.gradle
+++ b/testutils/testutils-datastore/build.gradle
@@ -23,14 +23,12 @@
*/
import androidx.build.LibraryType
-import static org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.*
-
plugins {
id("AndroidXPlugin")
}
androidXMultiplatform {
- jvm {}
+ jvm()
mac()
linux()
ios()
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
index 825a3ee..3ab85ce 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
@@ -44,6 +44,7 @@
* than the curved column, either at the [CurvedAlignment.Angular.Start] of the layout,
* at the [CurvedAlignment.Angular.End], or [CurvedAlignment.Angular.Center].
* If unspecified or null, they can choose for themselves.
+ * @param contentBuilder Scope used to provide the content for this column.
*/
public fun CurvedScope.curvedColumn(
modifier: CurvedModifier = CurvedModifier,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
index f4fa3ca..4f5daea 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
@@ -42,6 +42,7 @@
* and if those needs to be reversed in a Rtl layout.
* If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
* See [CurvedDirection.Angular].
+ * @param contentBuilder Scope used to provide the content for this row.
*/
public fun CurvedScope.curvedRow(
modifier: CurvedModifier = CurvedModifier,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
index e344c9f..2087c5c3 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
@@ -677,17 +677,19 @@
@RestrictTo(LIBRARY_GROUP)
public object SwipeableV2Defaults {
/**
- * The default animation used by [SwipeableV2State].
+ * The default animation that will be used to animate to a new state.
*/
val AnimationSpec = SpringSpec<Float>()
/**
- * The default velocity threshold (1.8 dp per millisecond) used by [rememberSwipeableV2State].
+ * The default velocity threshold (in dp per second) that the end velocity has to
+ * exceed in order to animate to the next state.
*/
val VelocityThreshold: Dp = 125.dp
/**
- * The default positional threshold (56 dp) used by [rememberSwipeableV2State]
+ * The default positional threshold used when calculating the target state while a swipe is in
+ * progress and when settling after the swipe ends.
*/
val PositionalThreshold: Density.(totalDistance: Float) -> Float =
fixedPositionalThreshold(56.dp)
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
index 1fdbc11..114fd30 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
@@ -310,10 +310,9 @@
// Canvas internally uses Spacer.drawBehind.
// Using Spacer.drawWithCache to optimize the stroke allocations.
Spacer(
+ // NB We must set the semantic role to Role.RadioButton in the parent Button,
+ // not here in the selection control - see b/330869742
modifier = modifier
- .semantics {
- this.role = Role.RadioButton
- }
.maybeSelectable(
onClick, enabled, selected, interactionSource, ripple, width, height
)
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
index 792bd00..3b0332a 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
@@ -211,6 +211,9 @@
indication = ripple,
interactionSource = interactionSource
)
+ // For a toggleable button, the role could be Checkbox or Switch,
+ // so we cannot set the semantics here. Instead,
+ // we set them in the toggle control
} else {
Modifier.selectable(
enabled = enabled,
@@ -218,7 +221,12 @@
onClick = { onCheckedChange(true) },
indication = ripple,
interactionSource = interactionSource
- )
+ ).semantics {
+ // For a selectable button, the role is always RadioButton.
+ // See also b/330869742 for issue with setting the RadioButton role
+ // within the selection control.
+ role = Role.RadioButton
+ }
}
)
.padding(contentPadding),
@@ -375,6 +383,12 @@
indication = ripple,
interactionSource = checkedInteractionSource
)
+ .semantics {
+ // For a selectable button, the role is always RadioButton.
+ // See also b/330869742 for issue with setting the RadioButton role
+ // within the selection control.
+ role = Role.RadioButton
+ }
}
Box(
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
index 2fde27b..c3d34d8 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
@@ -136,6 +136,28 @@
}
@Test
+ fun selectable_chip_has_role_radiobutton() {
+ rule.setContentWithTheme {
+ SelectableChip(
+ selected = true,
+ onClick = {},
+ enabled = false,
+ label = { Text("Label") },
+ selectionControl = { TestImage() },
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG)
+ .assert(
+ SemanticsMatcher.expectValue(
+ SemanticsProperties.Role,
+ Role.RadioButton
+ )
+ )
+ }
+
+ @Test
fun split_chip_has_clickaction_when_disabled() {
rule.setContentWithTheme {
SplitSelectableChip(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Card.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Card.kt
index fe84508..c9c736c 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Card.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Card.kt
@@ -90,6 +90,7 @@
* still happen internally.
* @param role The type of user interface element. Accessibility services might use this
* to describe the element or do customizations
+ * @param content Slot for composable body content displayed on the Card
*/
@Composable
public fun Card(
@@ -179,6 +180,7 @@
* set.
* @param timeColor The default color to use for time() slot unless explicitly set.
* @param titleColor The default color to use for title() slot unless explicitly set.
+ * @param content Slot for composable body content displayed on the Card
*/
@Composable
public fun AppCard(
@@ -289,6 +291,7 @@
* @param contentColor The default color to use for content() slot unless explicitly set.
* @param titleColor The default color to use for title() slot unless explicitly set.
* @param timeColor The default color to use for time() slot unless explicitly set.
+ * @param content Slot for composable body content displayed on the Card
*/
@Composable
public fun TitleCard(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt
index d80d069..5fed098 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt
@@ -172,6 +172,7 @@
* still happen internally.
* @param role The type of user interface element. Accessibility services might use this
* to describe the element or do customizations
+ * @param content Slot for composable body content displayed on the Chip
*/
@Composable
public fun Chip(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
index a7c7a0c..0566bf5 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
@@ -43,6 +43,7 @@
* @param modifier The modifier for the list header
* @param backgroundColor The background color to apply - typically Color.Transparent
* @param contentColor The color to apply to content
+ * @param content Slot for displayed header text
*/
@Composable
public fun ListHeader(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
index ed866b9..8f7bdbf 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
@@ -53,6 +53,7 @@
* @param colors A complete definition of the Wear Material Color theme for this hierarchy
* @param typography A set of text styles to be used as this hierarchy's typography system
* @param shapes A set of shapes to be used by the components in this hierarchy
+ * @param content Slot for composable content displayed with this theme
*/
@Composable
public fun MaterialTheme(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Scaffold.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Scaffold.kt
index e9a468b..fb7965a 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Scaffold.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Scaffold.kt
@@ -51,6 +51,7 @@
* page indicator is a pager with horizontally swipeable pages.
* @param timeText time and potential application status message to display at the top middle of the
* screen. Expected to be a TimeText component.
+ * @param content Slot for composable screen content
*/
@Composable
public fun Scaffold(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
index f5c3ef0..3997c14 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
@@ -82,6 +82,23 @@
}
@Test
+ fun radio_button_has_role_radiobutton() {
+ rule.setContentWithTheme {
+ RadioButtonWithDefaults(
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG)
+ .assert(
+ SemanticsMatcher.expectValue(
+ SemanticsProperties.Role,
+ Role.RadioButton
+ )
+ )
+ }
+
+ @Test
fun radio_button_samples_build() {
rule.setContentWithTheme {
RadioButton()
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
index 600e737..a8ac0da 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
@@ -22,10 +22,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
@@ -67,25 +63,6 @@
}
@Test
- fun radio_control_has_role_radiobutton() {
- rule.setContentWithTheme {
- with(SelectionControlScope(isEnabled = true, isSelected = true)) {
- Radio(
- modifier = Modifier.testTag(TEST_TAG)
- )
- }
- }
-
- rule.onNodeWithTag(TEST_TAG)
- .assert(
- SemanticsMatcher.expectValue(
- SemanticsProperties.Role,
- Role.RadioButton
- )
- )
- }
-
- @Test
fun radio_control_is_correctly_enabled() {
rule.setContentWithTheme {
with(SelectionControlScope(isEnabled = true, isSelected = true)) {
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index 2dc5958..3755e89 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -106,6 +106,7 @@
* emitting [Interaction]s for this button. You can use this to change the button's appearance
* or preview the button in different states. Note that if `null` is provided, interactions will
* still happen internally.
+ * @param content Slot for composable body content displayed on the Button
*/
@Composable
fun Button(
@@ -176,6 +177,7 @@
* emitting [Interaction]s for this button. You can use this to change the button's appearance
* or preview the button in different states. Note that if `null` is provided, interactions will
* still happen internally.
+ * @param content Slot for composable body content displayed on the Button
*/
@Composable
fun FilledTonalButton(
@@ -245,6 +247,7 @@
* emitting [Interaction]s for this button. You can use this to change the button's appearance
* or preview the button in different states. Note that if `null` is provided, interactions will
* still happen internally.
+ * @param content Slot for composable body content displayed on the OutlinedButton
*/
@Composable
fun OutlinedButton(
@@ -314,6 +317,7 @@
* emitting [Interaction]s for this button. You can use this to change the button's appearance
* or preview the button in different states. Note that if `null` is provided, interactions will
* still happen internally.
+ * @param content Slot for composable body content displayed on the ChildButton
*/
@Composable
fun ChildButton(
@@ -1058,6 +1062,8 @@
/**
* Creates a [BorderStroke], such as for an [OutlinedButton]
*
+ * @param enabled Controls the color of the border based on the enabled/disabled state of the
+ * button
* @param borderColor The color to use for the border for this outline when enabled
* @param disabledBorderColor The color to use for the border for this outline when
* disabled
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt
index 5ed024f..9ef0bde 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt
@@ -50,6 +50,7 @@
* @param colorScheme A complete definition of the Wear Material Color theme for this hierarchy
* @param typography A set of text styles to be used as this hierarchy's typography system
* @param shapes A set of shapes to be used by the components in this hierarchy
+ * @param content Slot for composable content displayed with this theme
*/
@Composable
fun MaterialTheme(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
index 70dbab71..b4a6f53 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
@@ -144,7 +144,13 @@
indication = rippleOrFallbackImplementation(),
interactionSource = interactionSource
)
- .padding(contentPadding),
+ .padding(contentPadding)
+ .semantics {
+ // For a selectable button, the role is always RadioButton.
+ // See also b/330869742 for issue with setting the RadioButton role
+ // within the selection control.
+ role = Role.RadioButton
+ },
verticalAlignment = Alignment.CenterVertically
) {
if (icon != null) {
@@ -329,7 +335,13 @@
.width(SPLIT_WIDTH)
.wrapContentHeight(align = Alignment.CenterVertically)
.wrapContentWidth(align = Alignment.End)
- .then(endPadding),
+ .then(endPadding)
+ .semantics {
+ // For a selectable button, the role is always RadioButton.
+ // See also b/330869742 for issue with setting the RadioButton role
+ // within the selection control.
+ role = Role.RadioButton
+ },
) {
val scope = remember(enabled, selected) { SelectionControlScope(enabled, selected) }
selectionControl(scope)
diff --git a/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt b/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
index 7981cf2..5b295fc 100644
--- a/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
+++ b/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
@@ -34,7 +34,7 @@
* note, the above list is not exhaustive. It previews the composables on a small round Wear device.
*
* @sample androidx.wear.compose.material.samples.TitleCardWithImagePreview
- * @see [Preview.fontScale]
+ * @see Preview.fontScale
*/
@Preview(
device = WearDevices.SMALL_ROUND,
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 28d0392..d68e962 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -25,8 +25,8 @@
defaultConfig {
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
- versionCode 23
- versionName "1.23"
+ versionCode 24
+ versionName "1.24"
}
buildTypes {
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index bd9565b..756cbe0 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -14,7 +14,7 @@
}
public static final class AnimationParameterBuilders.AnimationParameters.Builder {
- ctor public AnimationParameterBuilders.AnimationParameters.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationParameters.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDelayMillis(@IntRange(from=0) long);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDurationMillis(@IntRange(from=0) long);
@@ -27,10 +27,10 @@
}
public static final class AnimationParameterBuilders.AnimationSpec.Builder {
- ctor public AnimationParameterBuilders.AnimationSpec.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationSpec.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setAnimationParameters(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
- method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
}
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface AnimationParameterBuilders.Easing {
@@ -53,7 +53,7 @@
}
public static final class AnimationParameterBuilders.Repeatable.Builder {
- ctor public AnimationParameterBuilders.Repeatable.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.Repeatable.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setForwardRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setIterations(@IntRange(from=1) int);
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index bd9565b..756cbe0 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -14,7 +14,7 @@
}
public static final class AnimationParameterBuilders.AnimationParameters.Builder {
- ctor public AnimationParameterBuilders.AnimationParameters.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationParameters.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDelayMillis(@IntRange(from=0) long);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDurationMillis(@IntRange(from=0) long);
@@ -27,10 +27,10 @@
}
public static final class AnimationParameterBuilders.AnimationSpec.Builder {
- ctor public AnimationParameterBuilders.AnimationSpec.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationSpec.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setAnimationParameters(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
- method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
}
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface AnimationParameterBuilders.Easing {
@@ -53,7 +53,7 @@
}
public static final class AnimationParameterBuilders.Repeatable.Builder {
- ctor public AnimationParameterBuilders.Repeatable.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.Repeatable.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setForwardRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setIterations(@IntRange(from=1) int);
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
index 44eb9a7..a800b2a 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
@@ -140,6 +140,7 @@
AnimationParameterProto.AnimationSpec.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-2136602843);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets animation parameters including duration, easing and repeat delay. */
@@ -158,6 +159,7 @@
* Sets the repeatable mode to be used for specifying repetition parameters for the
* animation. If not set, animation won't be repeated.
*/
+ @RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setRepeatable(@NonNull Repeatable repeatable) {
mImpl.setRepeatable(repeatable.toProto());
@@ -261,6 +263,7 @@
AnimationParameterProto.AnimationParameters.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-1301308590);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
@@ -491,7 +494,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
AnimationParameterProto.CubicBezierEasing toProto() {
return mImpl;
@@ -525,6 +527,7 @@
AnimationParameterProto.CubicBezierEasing.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(856403705);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
@@ -704,6 +707,7 @@
AnimationParameterProto.Repeatable.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(2110475048);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
index 36e1b0b..5a088b9 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
@@ -48,12 +48,14 @@
}
/** Sets the value to use as the value when true in a conditional expression. */
- public @NonNull IfTrueScope<T, RawT> use(T valueWhenTrue) {
+ @NonNull
+ public IfTrueScope<T, RawT> use(T valueWhenTrue) {
return new IfTrueScope<>(valueWhenTrue, conditionBuilder, rawTypeMapper);
}
/** Sets the value to use as the value when true in a conditional expression. */
- public @NonNull IfTrueScope<T, RawT> use(RawT valueWhenTrue) {
+ @NonNull
+ public IfTrueScope<T, RawT> use(RawT valueWhenTrue) {
return use(rawTypeMapper.apply(valueWhenTrue));
}
}
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
index 17d89ce..500985d 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
@@ -708,7 +708,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -1255,7 +1255,7 @@
/** Sets the value to start animating from. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
- public AnimatableFixedInt32.Builder setFromValue(int fromValue) {
+ public Builder setFromValue(int fromValue) {
mImpl.setFromValue(fromValue);
mFingerprint.recordPropertyUpdate(1, fromValue);
return this;
@@ -1264,7 +1264,7 @@
/** Sets the value to animate to. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
- public AnimatableFixedInt32.Builder setToValue(int toValue) {
+ public Builder setToValue(int toValue) {
mImpl.setToValue(toValue);
mFingerprint.recordPropertyUpdate(2, toValue);
return this;
@@ -1398,7 +1398,7 @@
/** Sets the value to watch, and animate when it changes. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
- public AnimatableDynamicInt32.Builder setInput(@NonNull DynamicInt32 input) {
+ public Builder setInput(@NonNull DynamicInt32 input) {
mImpl.setInput(input.toDynamicInt32Proto());
mFingerprint.recordPropertyUpdate(
1, checkNotNull(input.getFingerprint()).aggregateValueAsInt());
@@ -2413,7 +2413,7 @@
/** Returns whether digit grouping is used or not. */
public boolean isGroupingUsed() {
- return mInt32FormatOp.getGroupingUsed();
+ return mInt32FormatOp.isGroupingUsed();
}
/** Builder to create {@link IntFormatter} objects. */
@@ -2579,7 +2579,7 @@
* locale. If not defined, defaults to false. For example, for locale en_US, using grouping
* with 1234 would yield "1,234".
*/
- public boolean getGroupingUsed() {
+ public boolean isGroupingUsed() {
return mImpl.getGroupingUsed();
}
@@ -2638,13 +2638,13 @@
+ ", minIntegerDigits="
+ getMinIntegerDigits()
+ ", groupingUsed="
- + getGroupingUsed()
+ + isGroupingUsed()
+ "}";
}
/** Builder for {@link Int32FormatOp}. */
public static final class Builder implements DynamicString.Builder {
- final DynamicProto.Int32FormatOp.Builder mImpl =
+ private final DynamicProto.Int32FormatOp.Builder mImpl =
DynamicProto.Int32FormatOp.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(196209833);
@@ -2792,7 +2792,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -3143,7 +3143,7 @@
* locale. If not defined, defaults to false. For example, for locale en_US, using grouping
* with 1234.56 would yield "1,234.56".
*/
- public boolean getGroupingUsed() {
+ public boolean isGroupingUsed() {
return mImpl.getGroupingUsed();
}
@@ -3206,7 +3206,7 @@
+ ", minIntegerDigits="
+ getMinIntegerDigits()
+ ", groupingUsed="
- + getGroupingUsed()
+ + isGroupingUsed()
+ "}";
}
@@ -3736,7 +3736,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -5012,7 +5012,7 @@
/** Returns whether digit grouping is used or not. */
public boolean isGroupingUsed() {
- return mFloatFormatOp.getGroupingUsed();
+ return mFloatFormatOp.isGroupingUsed();
}
/** Builder to create {@link FloatFormatter} objects. */
@@ -5228,8 +5228,8 @@
@NonNull
public DynamicProto.DynamicBool toDynamicBoolProto(boolean withFingerprint) {
if (withFingerprint) {
- return DynamicProto.DynamicBool.newBuilder().
- setStateSource(mImpl)
+ return DynamicProto.DynamicBool.newBuilder()
+ .setStateSource(mImpl)
.setFingerprint(checkNotNull(mFingerprint).toProto())
.build();
}
@@ -5265,7 +5265,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -6144,7 +6144,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
index 69c88e3..c3b5c45 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
@@ -28,7 +28,6 @@
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant;
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
-import androidx.wear.protolayout.expression.DynamicBuilders.DynamicType;
import androidx.wear.protolayout.expression.FixedValueBuilders.FixedBool;
import androidx.wear.protolayout.expression.FixedValueBuilders.FixedColor;
import androidx.wear.protolayout.expression.FixedValueBuilders.FixedDuration;
@@ -51,7 +50,7 @@
/** Interface defining a dynamic data value. */
@RequiresSchemaVersion(major = 1, minor = 200)
- public interface DynamicDataValue<T extends DynamicType> {
+ public interface DynamicDataValue<T extends DynamicBuilders.DynamicType> {
/** Get the protocol buffer representation of this object. */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@@ -310,7 +309,7 @@
/** Builder to create {@link DynamicDataValue} objects. */
@RestrictTo(Scope.LIBRARY_GROUP)
- interface Builder<T extends DynamicType> {
+ interface Builder<T extends DynamicBuilders.DynamicType> {
/** Builds an instance with values accumulated in this Builder. */
@NonNull
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
index ced6685..e16e059 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
@@ -78,7 +78,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedInt32 toProto() {
return mImpl;
@@ -144,6 +143,7 @@
private final FixedProto.FixedInt32.Builder mImpl = FixedProto.FixedInt32.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(974881783);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets the value. */
@@ -203,7 +203,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedString toProto() {
return mImpl;
@@ -271,6 +270,7 @@
FixedProto.FixedString.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(1963352072);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets the value. */
@@ -333,7 +333,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedFloat toProto() {
return mImpl;
@@ -399,6 +398,7 @@
private final FixedProto.FixedFloat.Builder mImpl = FixedProto.FixedFloat.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-144724541);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
@@ -461,7 +461,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedBool toProto() {
return mImpl;
@@ -527,6 +526,7 @@
private final FixedProto.FixedBool.Builder mImpl = FixedProto.FixedBool.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-665116398);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets the value. */
@@ -587,7 +587,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedColor toProto() {
return mImpl;
@@ -653,6 +652,7 @@
private final FixedProto.FixedColor.Builder mImpl = FixedProto.FixedColor.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-1895809356);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets the color value, in ARGB format. */
@@ -733,7 +733,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedInstant toProto() {
return mImpl;
@@ -778,6 +777,7 @@
FixedProto.FixedInstant.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-1986552556);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
@@ -860,7 +860,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedDuration toProto() {
return mImpl;
@@ -905,6 +904,7 @@
FixedProto.FixedDuration.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(9029504);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets duration in seconds. */
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
index 6d3cbd4..b36d18c 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
@@ -21,7 +21,6 @@
import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_END;
import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
-
import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.GAP_END_ANGLE;
import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.GAP_START_ANGLE;
@@ -406,7 +405,7 @@
"التسمية الأولية",
"نص اختباري.",
"نص طويل جدًا لا يمكن احتواؤه في المربع الأصلي الخاص به، لذا يجب تغيير حجمه بشكل"
- + " صحيح قبل السطر الأخير");
+ + " صحيح قبل السطر الأخير");
}
/**
@@ -415,7 +414,7 @@
* as it should point on the same size independent image.
*/
@NonNull
- @SuppressWarnings("deprecation") // TEXT_OVERFLOW_ELLIPSIZE_END
+ @SuppressWarnings("deprecation") // TEXT_OVERFLOW_ELLIPSIZE_END
private static ImmutableMap<String, Layout> generateTextTestCasesForLanguage(
@NonNull Context context,
@NonNull DeviceParameters deviceParameters,
@@ -439,18 +438,9 @@
.setColor(argb(Color.YELLOW))
.setWeight(LayoutElementBuilders.FONT_WEIGHT_BOLD)
.setTypography(Typography.TYPOGRAPHY_BODY2)
- .setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START)
.build());
testCases.put(
- "overflow_text_golden" + goldenSuffix,
- new Text.Builder(context, longText)
- .setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START)
- .build());
- testCases.put(
- "overflow_text_center_golden" + goldenSuffix,
- new Text.Builder(context, longText)
- .setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_CENTER)
- .build());
+ "overflow_text_golden" + goldenSuffix, new Text.Builder(context, longText).build());
testCases.put(
"overflow_ellipsize_maxlines_notreached" + goldenSuffix,
new Box.Builder()
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java
new file mode 100644
index 0000000..d6a206c
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License = 0 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 = 0 software
+ * distributed under the License is distributed on an "AS IS" BASIS = 0
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND = 0 either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Shared constants. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Constants {
+
+ private Constants() {}
+
+ /** The reason why an update was requested. */
+ @IntDef({
+ UPDATE_REQUEST_REASON_UNKNOWN,
+ UPDATE_REQUEST_REASON_SYSUI_CAROUSEL,
+ UPDATE_REQUEST_REASON_FRESHNESS,
+ UPDATE_REQUEST_REASON_USER_INTERACTION,
+ UPDATE_REQUEST_REASON_UPDATE_REQUESTER,
+ UPDATE_REQUEST_REASON_CACHE_INVALIDATION,
+ UPDATE_REQUEST_REASON_RETRY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UpdateRequestReason {}
+
+ /** Unknown reason. */
+ public static final int UPDATE_REQUEST_REASON_UNKNOWN = 0;
+
+ /** Update triggered by SysUI Carousel. */
+ public static final int UPDATE_REQUEST_REASON_SYSUI_CAROUSEL = 1;
+
+ /** Update triggered by freshness. */
+ public static final int UPDATE_REQUEST_REASON_FRESHNESS = 2;
+
+ /** Update triggered by user interaction (e.g. clicking on the tile). */
+ public static final int UPDATE_REQUEST_REASON_USER_INTERACTION = 3;
+
+ /** Update triggered using update requester. */
+ public static final int UPDATE_REQUEST_REASON_UPDATE_REQUESTER = 4;
+
+ /** Update triggered due to clearing the cache. */
+ public static final int UPDATE_REQUEST_REASON_CACHE_INVALIDATION = 5;
+
+ /** Update triggered by retry policy. */
+ public static final int UPDATE_REQUEST_REASON_RETRY = 6;
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java
new file mode 100644
index 0000000..f292c98
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.wear.protolayout.proto.StateProto.State;
+import androidx.wear.protolayout.renderer.common.Constants.UpdateRequestReason;
+
+/** A No-Op implementation of {@link ProviderStatsLogger}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class NoOpProviderStatsLogger implements ProviderStatsLogger {
+ private static final String TAG = "NoOpProviderStatsLogger";
+
+ /** Creates an instance of {@link NoOpProviderStatsLogger}. */
+ public NoOpProviderStatsLogger(@NonNull String reason) {
+ Log.i(TAG, "Instance used because " + reason);
+ }
+
+ /** No-op method. */
+ @Override
+ public void logLayoutSchemaVersion(int major, int minor) {}
+
+ /** No-op method. */
+ @Override
+ public void logStateStructure(@NonNull State state, boolean isInitialState) {}
+
+ /** No-op method. */
+ @Override
+ public void logIgnoredFailure(int failure) {}
+
+ /** No-op method. */
+ @Override
+ public void logInflationFailed(@InflationFailureReason int failureReason) {}
+
+ /** No-op method. */
+ @Override
+ @NonNull
+ public InflaterStatsLogger createInflaterStatsLogger() {
+ return new NoOpInflaterStatsLogger();
+ }
+
+ /** No-op method. */
+ @Override
+ public void logInflationFinished(@NonNull InflaterStatsLogger inflaterStatsLogger) {}
+
+ /** No-op method. */
+ @Override
+ public void logTileRequestReason(@UpdateRequestReason int updateRequestReason) {}
+
+ /** A No-Op implementation of {@link InflaterStatsLogger}. */
+ public static class NoOpInflaterStatsLogger implements InflaterStatsLogger {
+
+ private NoOpInflaterStatsLogger() {}
+
+ @Override
+ public void logMutationChangedNodes(int changedNodesCount) {}
+
+ @Override
+ public void logTotalNodeCount(int totalNodesCount) {}
+
+ /** No-op method. */
+ @Override
+ public void logDrawableUsage(@NonNull Drawable drawable) {}
+
+ /** No-op method. */
+ @Override
+ public void logIgnoredFailure(@IgnoredFailure int failure) {}
+
+ /** No-op method. */
+ @Override
+ public void logInflationFailed(@InflationFailureReason int failureReason) {}
+ }
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java
index 65f4fa2..6860d79 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java
@@ -35,7 +35,7 @@
import java.util.Collections;
import java.util.List;
-/** Utility to diff 2 proto layouts in order to be able to partially update the display. */
+/** Utility to diff two proto layouts in order to be able to partially update the display. */
@RestrictTo(Scope.LIBRARY_GROUP)
public class ProtoLayoutDiffer {
/** Prefix for all node IDs generated by this differ. */
@@ -63,7 +63,9 @@
@RestrictTo(Scope.LIBRARY_GROUP)
public static final int FIRST_CHILD_INDEX = 0;
- private enum NodeChangeType {
+ /** Type of the change applied to the node. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public enum NodeChangeType {
NO_CHANGE,
CHANGE_IN_SELF_ONLY,
CHANGE_IN_SELF_AND_ALL_CHILDREN,
@@ -108,8 +110,8 @@
}
@NonNull
- TreeNodeWithChange withChange(boolean isSelfOnlyChange) {
- return new TreeNodeWithChange(this, isSelfOnlyChange);
+ TreeNodeWithChange withChange(@NonNull NodeChangeType nodeChangeType) {
+ return new TreeNodeWithChange(this, nodeChangeType);
}
}
@@ -117,11 +119,11 @@
@RestrictTo(Scope.LIBRARY_GROUP)
public static final class TreeNodeWithChange {
@NonNull private final TreeNode mTreeNode;
- private final boolean mIsSelfOnlyChange;
+ @NonNull private final NodeChangeType mNodeChangeType;
- TreeNodeWithChange(@NonNull TreeNode treeNode, boolean isSelfOnlyChange) {
+ TreeNodeWithChange(@NonNull TreeNode treeNode, @NonNull NodeChangeType nodeChangeType) {
this.mTreeNode = treeNode;
- this.mIsSelfOnlyChange = isSelfOnlyChange;
+ this.mNodeChangeType = nodeChangeType;
}
/**
@@ -167,7 +169,20 @@
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public boolean isSelfOnlyChange() {
- return mIsSelfOnlyChange;
+ switch (mNodeChangeType) {
+ case CHANGE_IN_SELF_ONLY:
+ case CHANGE_IN_SELF_AND_SOME_CHILDREN:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /** Returns the {@link NodeChangeType} of this {@link TreeNodeWithChange}. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public NodeChangeType getChangeType() {
+ return mNodeChangeType;
}
}
@@ -295,15 +310,14 @@
@NonNull TreeNode node,
@NonNull List<TreeNodeWithChange> changedNodes)
throws InconsistentFingerprintException {
- switch (getChangeType(prevNodeFingerprint, node.mFingerprint)) {
+ NodeChangeType changeType = getChangeType(prevNodeFingerprint, node.mFingerprint);
+ switch (changeType) {
case CHANGE_IN_SELF_ONLY:
- changedNodes.add(node.withChange(/* isSelfOnlyChange= */ true));
- break;
case CHANGE_IN_SELF_AND_ALL_CHILDREN:
- changedNodes.add(node.withChange(/* isSelfOnlyChange= */ false));
+ changedNodes.add(node.withChange(changeType));
break;
case CHANGE_IN_SELF_AND_SOME_CHILDREN:
- changedNodes.add(node.withChange(/* isSelfOnlyChange= */ true));
+ changedNodes.add(node.withChange(changeType));
addChangedChildNodes(prevNodeFingerprint, node, changedNodes);
break;
case CHANGE_IN_CHILDREN:
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java
new file mode 100644
index 0000000..96a6fb0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.proto.StateProto.State;
+import androidx.wear.protolayout.renderer.common.Constants.UpdateRequestReason;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Logger used for collecting metrics. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ProviderStatsLogger {
+
+ /** Failures that doesn't cause the inflation to fail. */
+ @IntDef({
+ IGNORED_FAILURE_UNKNOWN,
+ IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION,
+ IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED,
+ IGNORED_FAILURE_DIFFING_FAILURE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface IgnoredFailure {}
+
+ /** Unknown failure. */
+ int IGNORED_FAILURE_UNKNOWN = 0;
+
+ /** Failure applying the diff mutation. */
+ int IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION = 1;
+
+ /** Failure caused by exceeding animation quota. */
+ int IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED = 2;
+
+ /** Failure diffing the layout. */
+ int IGNORED_FAILURE_DIFFING_FAILURE = 3;
+
+ /** Failures that causes the inflation to fail. */
+ @IntDef({
+ INFLATION_FAILURE_REASON_UNKNOWN,
+ INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED,
+ INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface InflationFailureReason {}
+
+ /** Unknown failure. */
+ int INFLATION_FAILURE_REASON_UNKNOWN = 0;
+
+ /** Failure caused by exceeding maximum layout depth. */
+ int INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED = 1;
+
+ /** Failure caused by exceeding maximum expression node count. */
+ int INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED = 2;
+
+ /** Log the schema version of the received layout. */
+ void logLayoutSchemaVersion(int major, int minor);
+
+ /** Log Protolayout state structure. */
+ void logStateStructure(@NonNull State state, boolean isInitialState);
+
+ /** Log the occurrence of an ignored failure. */
+ @UiThread
+ void logIgnoredFailure(@IgnoredFailure int failure);
+
+ /** Log the reason for inflation failure. */
+ @UiThread
+ void logInflationFailed(@InflationFailureReason int failureReason);
+
+ /**
+ * Creates an {@link InflaterStatsLogger} and marks the start of inflation. The atoms will be
+ * logged to statsd only when {@link #logInflationFinished} is called.
+ */
+ @UiThread
+ @NonNull
+ InflaterStatsLogger createInflaterStatsLogger();
+
+ /** Makes the end of inflation and log the inflation results. */
+ @UiThread
+ void logInflationFinished(@NonNull InflaterStatsLogger inflaterStatsLogger);
+
+ /** Log tile request reason. */
+ void logTileRequestReason(@UpdateRequestReason int updateRequestReason);
+
+ /** Logger used for logging inflation stats. */
+ interface InflaterStatsLogger {
+ /** log the mutation changed nodes count for the ongoing inflation. */
+ @UiThread
+ void logMutationChangedNodes(int changedNodesCount);
+
+ /** Log the total nodes count for the ongoing inflation. */
+ @UiThread
+ void logTotalNodeCount(int totalNodesCount);
+
+ /**
+ * Log the usage of a drawable. This method should be called between {@link
+ * #createInflaterStatsLogger()} and {@link #logInflationFinished(InflaterStatsLogger)}.
+ */
+ @UiThread
+ void logDrawableUsage(@NonNull Drawable drawable);
+
+ /**
+ * Log the occurrence of an ignored failure. The usage of this method is not restricted to
+ * inflation start or end.
+ */
+ @UiThread
+ void logIgnoredFailure(@IgnoredFailure int failure);
+
+ /**
+ * Log the reason for inflation failure. This will make any future call {@link
+ * #logInflationFinished(InflaterStatsLogger)} a Noop.
+ */
+ @UiThread
+ void logInflationFailed(@InflationFailureReason int failureReason);
+ }
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java
new file mode 100644
index 0000000..8424728
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 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.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+
+/** Artifacts resulted from the layout rendering. */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface RenderingArtifact {
+
+ /** Creates a {@link RenderingArtifact} instance. */
+ @NonNull
+ static RenderingArtifact create(@NonNull InflaterStatsLogger inflaterStatsLogger) {
+ return new SuccessfulRenderingArtifact(inflaterStatsLogger);
+ }
+
+ /** Creates a {@link RenderingArtifact} instance for a skipped inflation. */
+ @NonNull
+ static RenderingArtifact skipped() {
+ return new SkippedRenderingArtifact();
+ }
+
+ /** Creates a {@link RenderingArtifact} instance for a failed inflation. */
+ @NonNull
+ static RenderingArtifact failed() {
+ return new FailedRenderingArtifact();
+ }
+
+ /** Artifacts resulted from a successful layout rendering. */
+ class SuccessfulRenderingArtifact implements RenderingArtifact {
+ @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
+
+ private SuccessfulRenderingArtifact(@NonNull InflaterStatsLogger inflaterStatsLogger) {
+ mInflaterStatsLogger = inflaterStatsLogger;
+ }
+
+ /**
+ * Returns the {@link ProviderStatsLogger.InflaterStatsLogger} used log inflation stats.
+ * This will return {@code null} if the inflation was skipped or failed.
+ */
+ @NonNull
+ public InflaterStatsLogger getInflaterStatsLogger() {
+ return mInflaterStatsLogger;
+ }
+ }
+
+ /** Artifacts resulted from a skipped layout rendering. */
+ class SkippedRenderingArtifact implements RenderingArtifact {}
+
+ /** Artifacts resulted from a failed layout rendering. */
+ class FailedRenderingArtifact implements RenderingArtifact {}
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTree.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTree.java
index 18b5fa9..c2ad693 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTree.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTree.java
@@ -25,6 +25,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.VisibleForTesting;
import androidx.wear.protolayout.renderer.dynamicdata.PositionIdTree.TreeNode;
@@ -45,10 +47,11 @@
*
* <p>This class is not thread-safe.
*/
-final class PositionIdTree<T extends TreeNode> {
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class PositionIdTree<T extends TreeNode> {
/** Interface for nodes stored in this tree. */
- interface TreeNode {
+ public interface TreeNode {
/** Will be called after a node is removed from the tree. */
void destroy();
}
@@ -61,7 +64,7 @@
}
/** Removes all of the nodes in the tree and calls their {@link TreeNode#destroy()}. */
- void clear() {
+ public void clear() {
mPosIdToTreeNode.values().forEach(TreeNode::destroy);
mPosIdToTreeNode.clear();
}
@@ -71,7 +74,7 @@
* {@link TreeNode#destroy()} on all of the removed node. Note that the {@code posId} node won't
* be removed.
*/
- void removeChildNodesFor(@NonNull String posId) {
+ public void removeChildNodesFor(@NonNull String posId) {
removeChildNodesFor(posId, /* removeRoot= */ false);
}
@@ -92,7 +95,7 @@
* Adds the {@code newNode} to the tree. If the tree already contains a node at that position,
* the old node will be removed and will be destroyed.
*/
- void addOrReplace(@NonNull String posId, @NonNull T newNode) {
+ public void addOrReplace(@NonNull String posId, @NonNull T newNode) {
T oldNode = mPosIdToTreeNode.put(posId, newNode);
if (oldNode != null) {
oldNode.destroy();
@@ -107,16 +110,35 @@
/** Returns the node with {@code posId} or null if it doesn't exist. */
@Nullable
- T get(String posId) {
+ public T get(@NonNull String posId) {
return mPosIdToTreeNode.get(posId);
}
/**
- * Returns all of the ancestors of the node with {@code posId} matching the {@code predicate}.
+ * Returns all of the ancestors of the node {@code posId} and value matching the {@code
+ * predicate}.
*/
@NonNull
- List<T> findAncestorsFor(@NonNull String posId, @NonNull Predicate<? super T> predicate) {
+ public List<T> findAncestorsFor(
+ @NonNull String posId, @NonNull Predicate<? super T> predicate) {
List<T> result = new ArrayList<>();
+ for (String id : findAncestorsNodesFor(posId, predicate)) {
+ T value = mPosIdToTreeNode.get(id);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns all of the ancestors' posIds of the node {@code posId} with value matching the {@code
+ * predicate}.
+ */
+ @NonNull
+ public List<String> findAncestorsNodesFor(
+ @NonNull String posId, @NonNull Predicate<? super T> predicate) {
+ List<String> result = new ArrayList<>();
while (true) {
String parentPosId = getParentNodePosId(posId);
if (parentPosId == null) {
@@ -124,7 +146,7 @@
}
T value = mPosIdToTreeNode.get(parentPosId);
if (value != null && predicate.test(value)) {
- result.add(value);
+ result.add(parentPosId);
}
posId = parentPosId;
}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
index 08f0228..8f18ca8 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
@@ -20,6 +20,13 @@
import static android.widget.FrameLayout.LayoutParams.UNSPECIFIED_GRAVITY;
import static androidx.core.util.Preconditions.checkNotNull;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED;
+
+import static com.google.common.util.concurrent.Futures.immediateCancelledFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
import android.content.Context;
import android.content.res.Resources;
@@ -41,6 +48,7 @@
import androidx.wear.protolayout.expression.PlatformDataKey;
import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
import androidx.wear.protolayout.expression.pipeline.PlatformDataProvider;
+import androidx.wear.protolayout.expression.pipeline.QuotaManager;
import androidx.wear.protolayout.expression.pipeline.StateStore;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement.InnerCase;
@@ -52,7 +60,11 @@
import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
import androidx.wear.protolayout.renderer.ProtoLayoutVisibilityState;
import androidx.wear.protolayout.renderer.common.LoggingUtils;
+import androidx.wear.protolayout.renderer.common.NoOpProviderStatsLogger;
import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
@@ -114,6 +126,7 @@
@NonNull private final ListeningExecutorService mUiExecutorService;
@NonNull private final ListeningExecutorService mBgExecutorService;
@NonNull private final String mClickableIdExtra;
+ @NonNull private final ProviderStatsLogger mProviderStatsLogger;
@Nullable private final LoggingUtils mLoggingUtils;
@Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
@@ -219,10 +232,11 @@
*/
@UiThread
@NonNull
- ListenableFuture<Void> postInflate(
+ ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching);
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger);
}
/** Result of a {@link #renderOrComputeMutations} call when no changes are required. */
@@ -234,11 +248,12 @@
@NonNull
@Override
- public ListenableFuture<Void> postInflate(
+ public ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching) {
- return Futures.immediateVoidFuture();
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger) {
+ return immediateFuture(RenderingArtifact.create(inflaterStatsLogger));
}
}
@@ -251,11 +266,12 @@
@NonNull
@Override
- public ListenableFuture<Void> postInflate(
+ public ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching) {
- return Futures.immediateVoidFuture();
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger) {
+ return immediateFuture(RenderingArtifact.failed());
}
}
@@ -278,10 +294,11 @@
@NonNull
@Override
@UiThread
- public ListenableFuture<Void> postInflate(
+ public ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching) {
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger) {
InflateResult inflateResult =
checkNotNull(
mNewInflateParentData.mInflateResult,
@@ -292,7 +309,7 @@
attachParent.addView(
inflateResult.inflateParent, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
inflateResult.updateDynamicDataPipeline(isReattaching);
- return Futures.immediateVoidFuture();
+ return immediateFuture(RenderingArtifact.create(inflaterStatsLogger));
}
}
@@ -318,10 +335,11 @@
@NonNull
@Override
@UiThread
- public ListenableFuture<Void> postInflate(
+ public ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching) {
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger) {
return mInflater.applyMutation(checkNotNull(prevInflateParent), mMutation);
}
}
@@ -345,6 +363,7 @@
@NonNull private final String mClickableIdExtra;
@Nullable private final LoggingUtils mLoggingUtils;
+ @NonNull private final ProviderStatsLogger mProviderStatsLogger;
private final boolean mAnimationEnabled;
private final int mRunningAnimationsLimit;
@@ -366,6 +385,7 @@
@Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
@NonNull String clickableIdExtra,
@Nullable LoggingUtils loggingUtils,
+ @NonNull ProviderStatsLogger providerStatsLogger,
boolean animationEnabled,
int runningAnimationsLimit,
boolean updatesEnabled,
@@ -384,6 +404,7 @@
this.mExtensionViewProvider = extensionViewProvider;
this.mClickableIdExtra = clickableIdExtra;
this.mLoggingUtils = loggingUtils;
+ this.mProviderStatsLogger = providerStatsLogger;
this.mAnimationEnabled = animationEnabled;
this.mRunningAnimationsLimit = runningAnimationsLimit;
this.mUpdatesEnabled = updatesEnabled;
@@ -468,6 +489,13 @@
return mLoggingUtils;
}
+ /** Returns the provider stats logger used for telemetry. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public ProviderStatsLogger getProviderStatsLogger() {
+ return mProviderStatsLogger;
+ }
+
/** Returns whether animations are enabled. */
@RestrictTo(Scope.LIBRARY)
public boolean getAnimationEnabled() {
@@ -529,6 +557,7 @@
@Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider;
@NonNull private final String mClickableIdExtra;
@Nullable private LoggingUtils mLoggingUtils;
+ @Nullable private ProviderStatsLogger mProviderStatsLogger;
private boolean mAnimationEnabled = true;
private int mRunningAnimationsLimit = DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS;
@@ -632,6 +661,15 @@
return this;
}
+ /** Sets the provider stats logger used for telemetry. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setProviderStatsLogger(
+ @NonNull ProviderStatsLogger providerStatsLogger) {
+ this.mProviderStatsLogger = providerStatsLogger;
+ return this;
+ }
+
/**
* Sets whether animation are enabled. If disabled, none of the animation will be
* played.
@@ -715,6 +753,12 @@
if (mRendererResources == null) {
this.mRendererResources = mUiContext.getResources();
}
+
+ if (mProviderStatsLogger == null) {
+ mProviderStatsLogger =
+ new NoOpProviderStatsLogger(
+ "ProviderStatsLogger not provided to " + TAG);
+ }
return new Config(
mUiContext,
mRendererResources,
@@ -728,6 +772,7 @@
mExtensionViewProvider,
mClickableIdExtra,
mLoggingUtils,
+ mProviderStatsLogger,
mAnimationEnabled,
mRunningAnimationsLimit,
mUpdatesEnabled,
@@ -754,24 +799,51 @@
this.mWasFullyVisibleBefore = false;
this.mAllowLayoutChangingBindsWithoutDefault =
config.getAllowLayoutChangingBindsWithoutDefault();
+ this.mProviderStatsLogger = config.getProviderStatsLogger();
StateStore stateStore = config.getStateStore();
- if (stateStore != null) {
- mDataPipeline =
- config.getAnimationEnabled()
- ? new ProtoLayoutDynamicDataPipeline(
- config.getPlatformDataProviders(),
- stateStore,
- new FixedQuotaManagerImpl(
- config.getRunningAnimationsLimit(), "animations"),
- new FixedQuotaManagerImpl(
- DYNAMIC_NODES_MAX_COUNT, "dynamic nodes"))
- : new ProtoLayoutDynamicDataPipeline(
- config.getPlatformDataProviders(), stateStore);
- mDataPipeline.setFullyVisible(config.getIsViewFullyVisible());
- } else {
+ if (stateStore == null) {
mDataPipeline = null;
+ return;
}
+
+ if (config.getAnimationEnabled()) {
+ QuotaManager nodeQuotaManager =
+ new FixedQuotaManagerImpl(DYNAMIC_NODES_MAX_COUNT, "dynamic nodes") {
+ @Override
+ public boolean tryAcquireQuota(int quota) {
+ boolean success = super.tryAcquireQuota(quota);
+ if (!success) {
+ mProviderStatsLogger.logInflationFailed(
+ INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED);
+ }
+ return success;
+ }
+ };
+ mDataPipeline =
+ new ProtoLayoutDynamicDataPipeline(
+ config.getPlatformDataProviders(),
+ stateStore,
+ new FixedQuotaManagerImpl(
+ config.getRunningAnimationsLimit(), "animations") {
+ @Override
+ public boolean tryAcquireQuota(int quota) {
+ boolean success = super.tryAcquireQuota(quota);
+ if (!success) {
+ mProviderStatsLogger.logIgnoredFailure(
+ IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED);
+ }
+ return success;
+ }
+ },
+ nodeQuotaManager);
+ } else {
+ mDataPipeline =
+ new ProtoLayoutDynamicDataPipeline(
+ config.getPlatformDataProviders(), stateStore);
+ }
+
+ mDataPipeline.setFullyVisible(config.getIsViewFullyVisible());
}
@WorkerThread
@@ -780,7 +852,8 @@
@NonNull Layout layout,
@NonNull ResourceProto.Resources resources,
@Nullable RenderedMetadata prevRenderedMetadata,
- @NonNull ViewProperties parentViewProp) {
+ @NonNull ViewProperties parentViewProp,
+ @NonNull InflaterStatsLogger inflaterStatsLogger) {
ResourceResolvers resolvers =
mResourceResolversProvider.getResourceResolvers(
mUiContext, resources, mUiExecutorService, mAnimationEnabled);
@@ -797,10 +870,10 @@
if (sameFingerprint) {
if (mPrevLayoutAlreadyFailingDepthCheck) {
- throwExceptionForLayoutDepthCheckFailure();
+ handleLayoutDepthCheckFailure(inflaterStatsLogger);
}
} else {
- checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH);
+ checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH, inflaterStatsLogger);
}
mPrevLayoutAlreadyFailingDepthCheck = false;
@@ -815,6 +888,7 @@
.setClickableIdExtra(mClickableIdExtra)
.setAllowLayoutChangingBindsWithoutDefault(
mAllowLayoutChangingBindsWithoutDefault)
+ .setInflaterStatsLogger(inflaterStatsLogger)
.setApplyFontVariantBodyAsDefault(true);
if (mDataPipeline != null) {
inflaterConfigBuilder.setDynamicDataPipeline(mDataPipeline);
@@ -886,6 +960,18 @@
return new InflateParentData(result);
}
+ @UiThread
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public ListenableFuture<RenderingArtifact> renderLayoutAndAttach(
+ @NonNull Layout layout,
+ @NonNull ResourceProto.Resources resources,
+ @NonNull ViewGroup attachParent) {
+
+ return renderAndAttach(
+ layout, resources, attachParent, mProviderStatsLogger.createInflaterStatsLogger());
+ }
+
/**
* Render the layout for this layout and attach this layout instance to a {@code attachParent}
* container. Note that this method may clear all of {@code attachParent}'s children before
@@ -911,6 +997,47 @@
@NonNull Layout layout,
@NonNull ResourceProto.Resources resources,
@NonNull ViewGroup attachParent) {
+ SettableFuture<Void> result = SettableFuture.create();
+ ListenableFuture<RenderingArtifact> future =
+ renderLayoutAndAttach(layout, resources, attachParent);
+ if (future.isDone()) {
+ if (future.isCancelled()) {
+ return immediateCancelledFuture();
+ }
+ return immediateFuture(null);
+ } else {
+ future.addListener(
+ () -> {
+ if (future.isCancelled()) {
+ result.cancel(/* mayInterruptIfRunning= */ false);
+ } else {
+ try {
+ RenderingArtifact ignored = future.get();
+ result.set(null);
+ } catch (ExecutionException
+ | InterruptedException
+ | CancellationException e) {
+ Log.e(TAG, "Failed to render layout", e);
+ result.setException(e);
+ }
+ }
+ },
+ mUiExecutorService);
+ }
+ return result;
+ }
+
+ @UiThread
+ @SuppressWarnings({
+ "ReferenceEquality",
+ "ExecutorTaskName",
+ }) // layout == prevLayout is intentional (and enough in this case)
+ @NonNull
+ private ListenableFuture<RenderingArtifact> renderAndAttach(
+ @NonNull Layout layout,
+ @NonNull ResourceProto.Resources resources,
+ @NonNull ViewGroup attachParent,
+ @NonNull InflaterStatsLogger inflaterStatsLogger) {
if (mLoggingUtils != null && mLoggingUtils.canLogD(TAG)) {
mLoggingUtils.logD(TAG, "Layout received in #renderAndAttach:\n %s", layout.toString());
mLoggingUtils.logD(
@@ -930,7 +1057,7 @@
if (layout == mPrevLayout && mInflateParent != null) {
// Nothing to do.
- return Futures.immediateVoidFuture();
+ return Futures.immediateFuture(RenderingArtifact.skipped());
}
boolean isReattaching = false;
@@ -1000,10 +1127,11 @@
layout,
resources,
prevRenderedMetadata,
- parentViewProp));
+ parentViewProp,
+ inflaterStatsLogger));
mCanReattachWithoutRendering = false;
}
- SettableFuture<Void> result = SettableFuture.create();
+ SettableFuture<RenderingArtifact> result = SettableFuture.create();
if (!checkNotNull(mRenderFuture).isDone()) {
ListenableFuture<RenderResult> rendererFuture = mRenderFuture;
mRenderFuture.addListener(
@@ -1023,7 +1151,8 @@
checkNotNull(rendererFuture).get(),
/* isReattaching= */ false,
layout,
- resources));
+ resources,
+ inflaterStatsLogger));
} catch (ExecutionException
| InterruptedException
| CancellationException e) {
@@ -1048,7 +1177,8 @@
mRenderFuture.get(),
isReattaching,
layout,
- resources));
+ resources,
+ inflaterStatsLogger));
} catch (ExecutionException | InterruptedException | CancellationException e) {
Log.e(TAG, "Failed to render layout", e);
result.setException(e);
@@ -1064,6 +1194,12 @@
*/
public void invalidateCache() {
mPrevResourcesVersion = null;
+ // Cancel any ongoing rendering which might have a reference to older app resources.
+ if (mRenderFuture != null && !mRenderFuture.isDone()) {
+ mRenderFuture.cancel(/* mayInterruptIfRunning= */ false);
+ mRenderFuture = null;
+ Log.w(TAG, "Cancelled ongoing rendering due to cache invalidation.");
+ }
}
@Nullable
@@ -1080,13 +1216,14 @@
@UiThread
@SuppressWarnings("ExecutorTaskName")
@NonNull
- private ListenableFuture<Void> postInflate(
+ private ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
@NonNull RenderResult renderResult,
boolean isReattaching,
@NonNull Layout layout,
- @NonNull ResourceProto.Resources resources) {
+ @NonNull ResourceProto.Resources resources,
+ InflaterStatsLogger inflaterStatsLogger) {
mCanReattachWithoutRendering = renderResult.canReattachWithoutRendering();
if (renderResult instanceof InflatedIntoNewParentRenderResult) {
@@ -1101,9 +1238,10 @@
.inflateParent;
}
- ListenableFuture<Void> postInflateFuture =
- renderResult.postInflate(attachParent, prevInflateParent, isReattaching);
- SettableFuture<Void> result = SettableFuture.create();
+ ListenableFuture<RenderingArtifact> postInflateFuture =
+ renderResult.postInflate(
+ attachParent, prevInflateParent, isReattaching, inflaterStatsLogger);
+ SettableFuture<RenderingArtifact> result = SettableFuture.create();
if (!postInflateFuture.isDone()) {
postInflateFuture.addListener(
() -> {
@@ -1114,20 +1252,24 @@
| CancellationException e) {
result.setFuture(
handlePostInflateFailure(
- e, layout, resources, prevInflateParent, attachParent));
+ e,
+ layout,
+ resources,
+ prevInflateParent,
+ attachParent,
+ inflaterStatsLogger));
}
},
mUiExecutorService);
} else {
try {
- postInflateFuture.get();
- return Futures.immediateVoidFuture();
+ return immediateFuture(postInflateFuture.get());
} catch (ExecutionException
| InterruptedException
| CancellationException
| ViewMutationException e) {
return handlePostInflateFailure(
- e, layout, resources, prevInflateParent, attachParent);
+ e, layout, resources, prevInflateParent, attachParent, inflaterStatsLogger);
}
}
return result;
@@ -1136,22 +1278,24 @@
@UiThread
@SuppressWarnings("ReferenceEquality") // layout == prevLayout is intentional
@NonNull
- private ListenableFuture<Void> handlePostInflateFailure(
+ private ListenableFuture<RenderingArtifact> handlePostInflateFailure(
@NonNull Throwable error,
@NonNull Layout layout,
@NonNull ResourceProto.Resources resources,
@Nullable ViewGroup prevInflateParent,
- @NonNull ViewGroup parent) {
+ @NonNull ViewGroup parent,
+ InflaterStatsLogger inflaterStatsLogger) {
// If a RuntimeError is thrown, it'll be wrapped in an UncheckedExecutionException
Throwable e = error.getCause();
if (e instanceof ViewMutationException) {
+ inflaterStatsLogger.logIgnoredFailure(IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION);
Log.w(TAG, "applyMutation failed." + e.getMessage());
if (mPrevLayout == layout && parent == mAttachParent) {
Log.w(TAG, "Retrying full inflation.");
// Clear rendering metadata and prevLayout to force a full reinflation.
ProtoLayoutInflater.clearRenderedMetadata(checkNotNull(prevInflateParent));
mPrevLayout = null;
- return renderAndAttach(layout, resources, parent);
+ return renderAndAttach(layout, resources, parent, inflaterStatsLogger);
}
} else {
Log.e(TAG, "postInflate failed.", error);
@@ -1176,6 +1320,7 @@
private void detachInternal() {
if (mRenderFuture != null && !mRenderFuture.isDone()) {
mRenderFuture.cancel(/* mayInterruptIfRunning= */ false);
+ mRenderFuture = null;
}
setLayoutVisibility(ProtoLayoutVisibilityState.VISIBILITY_STATE_INVISIBLE);
@@ -1227,9 +1372,12 @@
}
/** Returns true if the layout element depth doesn't exceed the given {@code allowedDepth}. */
- private void checkLayoutDepth(LayoutElement layoutElement, int allowedDepth) {
+ private void checkLayoutDepth(
+ LayoutElement layoutElement,
+ int allowedDepth,
+ InflaterStatsLogger inflaterStatsLogger) {
if (allowedDepth <= 0) {
- throwExceptionForLayoutDepthCheckFailure();
+ handleLayoutDepthCheckFailure(inflaterStatsLogger);
}
List<LayoutElement> children = ImmutableList.of();
switch (layoutElement.getInnerCase()) {
@@ -1245,28 +1393,32 @@
case ARC:
List<ArcLayoutElement> arcElements = layoutElement.getArc().getContentsList();
if (!arcElements.isEmpty() && allowedDepth == 1) {
- throwExceptionForLayoutDepthCheckFailure();
+ handleLayoutDepthCheckFailure(inflaterStatsLogger);
}
for (ArcLayoutElement element : arcElements) {
if (element.getInnerCase() == InnerCase.ADAPTER) {
- checkLayoutDepth(element.getAdapter().getContent(), allowedDepth - 1);
+ checkLayoutDepth(
+ element.getAdapter().getContent(),
+ allowedDepth - 1,
+ inflaterStatsLogger);
}
}
break;
case SPANNABLE:
if (layoutElement.getSpannable().getSpansCount() > 0 && allowedDepth == 1) {
- throwExceptionForLayoutDepthCheckFailure();
+ handleLayoutDepthCheckFailure(inflaterStatsLogger);
}
break;
default:
// Other LayoutElements have depth of one.
}
for (LayoutElement child : children) {
- checkLayoutDepth(child, allowedDepth - 1);
+ checkLayoutDepth(child, allowedDepth - 1, inflaterStatsLogger);
}
}
- private void throwExceptionForLayoutDepthCheckFailure() {
+ private void handleLayoutDepthCheckFailure(InflaterStatsLogger inflaterStatsLogger) {
+ inflaterStatsLogger.logInflationFailed(INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED);
mPrevLayoutAlreadyFailingDepthCheck = true;
throw new IllegalStateException(
"Layout depth exceeds maximum allowed depth: " + MAX_LAYOUT_ELEMENT_DEPTH);
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpan.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpan.java
deleted file mode 100644
index 40db96c..0000000
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpan.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 2024 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.
- */
-
-package androidx.wear.protolayout.renderer.inflater;
-
-import static java.lang.Math.abs;
-
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.text.Layout;
-import android.text.Layout.Alignment;
-import android.text.StaticLayout;
-import android.text.style.LeadingMarginSpan;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-/**
- * Helper class fixing the indentation for the last broken line by translating the canvas in the
- * opposite direction.
- *
- * <p>Applying letter spacing, center alignment and ellipsis to a text causes incorrect indentation
- * of the truncated line. For example, the last line is indented in a way where the start of the
- * line is outside of the boundaries of text.
- *
- * <p>It should be applied to a text only when those three attributes are set.
- */
-// Branched from androidx.compose.ui.text.android.style.IndentationFixSpan
-class IndentationFixSpan implements LeadingMarginSpan {
- @VisibleForTesting static final String ELLIPSIS_CHAR = "…";
- @Nullable private Layout mOverrideLayoutForMeasuring = null;
-
- @Override
- public int getLeadingMargin(boolean first) {
- return 0;
- }
-
- /**
- * Creates an instance of {@link IndentationFixSpan} used for fixing the text in {@link
- * android.widget.TextView} when ellipsize, letter spacing and alignment are set.
- */
- IndentationFixSpan() {}
-
- /**
- * Creates an instance of {@link IndentationFixSpan} used for fixing the text in {@link
- * android.widget.TextView} when ellipsize, letter spacing and alignment are set.
- *
- * @param layout The {@link StaticLayout} used for measuring how much Canvas should be rotated
- * in {@link #drawLeadingMargin}.
- */
- IndentationFixSpan(@NonNull StaticLayout layout) {
- this.mOverrideLayoutForMeasuring = layout;
- }
-
- /**
- * See {@link LeadingMarginSpan#drawLeadingMargin}.
- *
- * <p>If {@code IndentationFixSpan(StaticLayout)} has been used, the given {@code layout} would
- * be ignored when doing measurements.
- */
- @Override
- public void drawLeadingMargin(
- @NonNull Canvas canvas,
- @Nullable Paint paint,
- int x,
- int dir,
- int top,
- int baseline,
- int bottom,
- @Nullable CharSequence text,
- int start,
- int end,
- boolean first,
- @Nullable Layout layout) {
- // If StaticLayout has been provided, we should use that one for measuring instead of the
- // passed in one.
- if (mOverrideLayoutForMeasuring != null) {
- layout = mOverrideLayoutForMeasuring;
- }
-
- if (layout == null || paint == null) {
- return;
- }
-
- float padding = calculatePadding(paint, start, layout);
-
- if (padding != 0f) {
- canvas.translate(padding, 0f);
- }
- }
-
- /** Calculates the extra padding on ellipsized last line. Otherwise, returns 0. */
- @VisibleForTesting
- static float calculatePadding(@NonNull Paint paint, int start, @NonNull Layout layout) {
- int lineIndex = layout.getLineForOffset(start);
-
- // No action needed if line is not ellipsized and that is not the last line.
- if (lineIndex != layout.getLineCount() - 1 || !isLineEllipsized(layout, lineIndex)) {
- return 0f;
- }
-
- return layout.getParagraphDirection(lineIndex) == Layout.DIR_LEFT_TO_RIGHT
- ? getEllipsizedPaddingForLtr(layout, lineIndex, paint)
- : getEllipsizedPaddingForRtl(layout, lineIndex, paint);
- }
-
- /** Returns whether the given line is ellipsized. */
- private static boolean isLineEllipsized(@NonNull Layout layout, int lineIndex) {
- return layout.getEllipsisCount(lineIndex) > 0;
- }
-
- /**
- * Gets the extra padding that is on the left when line is ellipsized on left-to-right layout
- * direction. Otherwise, returns 0.
- */
- private static float getEllipsizedPaddingForLtr(
- @NonNull Layout layout, int lineIndex, @NonNull Paint paint) {
- float lineLeft = layout.getLineLeft(lineIndex);
-
- if (lineLeft >= 0) {
- return 0;
- }
-
- int ellipsisIndex = getEllipsisIndex(layout, lineIndex);
- float horizontal = getHorizontalPosition(layout, ellipsisIndex);
- float length = (horizontal - lineLeft) + paint.measureText(ELLIPSIS_CHAR);
- float divideFactor = getDivideFactor(layout, lineIndex);
-
- return abs(lineLeft) + ((layout.getWidth() - length) / divideFactor);
- }
-
- /**
- * Gets the extra padding that is on the right when line is ellipsized on right-to-left layout
- * direction. Otherwise, returns 0.
- */
- // TODO: b/323180070 - Investigate how to improve this so that text doesn't get clipped on large
- // sizes as there is a bug in platform with letter spacing on formatting characters.
- private static float getEllipsizedPaddingForRtl(
- @NonNull Layout layout, int lineIndex, @NonNull Paint paint) {
- float width = layout.getWidth();
-
- if (width >= layout.getLineRight(lineIndex)) {
- return 0;
- }
-
- int ellipsisIndex = getEllipsisIndex(layout, lineIndex);
- float horizontal = getHorizontalPosition(layout, ellipsisIndex);
- float length = (layout.getLineRight(lineIndex) - horizontal)
- + paint.measureText(ELLIPSIS_CHAR);
- float divideFactor = getDivideFactor(layout, lineIndex);
-
- return width - layout.getLineRight(lineIndex) - ((width - length) / divideFactor);
- }
-
- private static float getHorizontalPosition(@NonNull Layout layout, int ellipsisIndex) {
- return layout.getPrimaryHorizontal(ellipsisIndex);
- }
-
- private static int getEllipsisIndex(@NonNull Layout layout, int lineIndex) {
- return layout.getLineStart(lineIndex) + layout.getEllipsisStart(lineIndex);
- }
-
- private static float getDivideFactor(@NonNull Layout layout, int lineIndex) {
- return layout.getParagraphAlignment(lineIndex) == Alignment.ALIGN_CENTER ? 2f : 1f;
- }
-}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index 04eab6a..4997dde 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -28,7 +28,7 @@
import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.getParentNodePosId;
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
-import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
import static java.lang.Math.max;
import static java.lang.Math.min;
@@ -53,7 +53,6 @@
import android.graphics.drawable.GradientDrawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
-import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
@@ -181,9 +180,12 @@
import androidx.wear.protolayout.renderer.ProtoLayoutTheme.FontSet;
import androidx.wear.protolayout.renderer.R;
import androidx.wear.protolayout.renderer.common.LoggingUtils;
+import androidx.wear.protolayout.renderer.common.NoOpProviderStatsLogger;
import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer;
import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.LayoutDiff;
import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.TreeNodeWithChange;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.LayoutInfo;
@@ -302,6 +304,7 @@
final String mClickableIdExtra;
@Nullable private final LoggingUtils mLoggingUtils;
+ @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
@Nullable final Executor mLoadActionExecutor;
final LoadActionListener mLoadActionListener;
@@ -529,6 +532,7 @@
@NonNull private final String mClickableIdExtra;
@Nullable private final LoggingUtils mLoggingUtils;
+ @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
@Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
private final boolean mAnimationEnabled;
@@ -548,6 +552,7 @@
@Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
@NonNull String clickableIdExtra,
@Nullable LoggingUtils loggingUtils,
+ @NonNull InflaterStatsLogger inflaterStatsLogger,
boolean animationEnabled,
boolean allowLayoutChangingBindsWithoutDefault,
boolean applyFontVariantBodyAsDefault) {
@@ -563,6 +568,7 @@
this.mAllowLayoutChangingBindsWithoutDefault = allowLayoutChangingBindsWithoutDefault;
this.mClickableIdExtra = clickableIdExtra;
this.mLoggingUtils = loggingUtils;
+ this.mInflaterStatsLogger = inflaterStatsLogger;
this.mExtensionViewProvider = extensionViewProvider;
this.mApplyFontVariantBodyAsDefault = applyFontVariantBodyAsDefault;
}
@@ -639,6 +645,12 @@
return mLoggingUtils;
}
+ /** Stats logger used for telemetry. */
+ @NonNull
+ public InflaterStatsLogger getInflaterStatsLogger() {
+ return mInflaterStatsLogger;
+ }
+
/** View provider for the renderer extension. */
@Nullable
public ProtoLayoutExtensionViewProvider getExtensionViewProvider() {
@@ -679,6 +691,7 @@
@Nullable private String mClickableIdExtra;
@Nullable private LoggingUtils mLoggingUtils;
+ @Nullable private InflaterStatsLogger mInflaterStatsLogger;
@Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider = null;
@@ -789,6 +802,14 @@
return this;
}
+ /** Sets the stats logger used for telemetry. */
+ @NonNull
+ public Builder setInflaterStatsLogger(
+ @NonNull InflaterStatsLogger inflaterStatsLogger) {
+ this.mInflaterStatsLogger = inflaterStatsLogger;
+ return this;
+ }
+
/**
* Sets whether a "layout changing" data bind can be applied without the
* "value_for_layout" field being filled in, or being set to zero / empty. Defaults to
@@ -835,7 +856,11 @@
if (mClickableIdExtra == null) {
mClickableIdExtra = DEFAULT_CLICKABLE_ID_EXTRA;
}
-
+ if (mInflaterStatsLogger == null) {
+ mInflaterStatsLogger =
+ new NoOpProviderStatsLogger("No implementation was provided")
+ .createInflaterStatsLogger();
+ }
return new Config(
mUiContext,
mLayout,
@@ -848,6 +873,7 @@
mExtensionViewProvider,
checkNotNull(mClickableIdExtra),
mLoggingUtils,
+ mInflaterStatsLogger,
mAnimationEnabled,
mAllowLayoutChangingBindsWithoutDefault,
mApplyFontVariantBodyAsDefault);
@@ -874,6 +900,7 @@
config.getAllowLayoutChangingBindsWithoutDefault();
this.mClickableIdExtra = config.getClickableIdExtra();
this.mLoggingUtils = config.getLoggingUtils();
+ this.mInflaterStatsLogger = config.getInflaterStatsLogger();
this.mExtensionViewProvider = config.getExtensionViewProvider();
this.mApplyFontVariantBodyAsDefault = config.getApplyFontVariantBodyAsDefault();
}
@@ -1732,7 +1759,9 @@
if (modifiers.hasTransformation()) {
applyTransformation(
- wrapper == null ? view : wrapper, modifiers.getTransformation(), posId,
+ wrapper == null ? view : wrapper,
+ modifiers.getTransformation(),
+ posId,
pipelineMaker);
}
@@ -2566,8 +2595,7 @@
}
@Override
- public void onViewDetachedFromWindow(@NonNull View v) {
- }
+ public void onViewDetachedFromWindow(@NonNull View v) {}
});
}
@@ -2673,10 +2701,13 @@
text.getText(),
t -> {
// Underlines are applied using a Spannable here, rather than setting paint bits
- // (or using Paint#setTextUnderline). When multiple fonts are mixed on the same
- // line (especially when mixing anything with NotoSans-CJK), multiple
- // underlines can appear. Using UnderlineSpan instead though causes the
- // correct behaviour to happen (only a single underline).
+ // (or
+ // using Paint#setTextUnderline). When multiple fonts are mixed on the same line
+ // (especially when mixing anything with NotoSans-CJK), multiple underlines can
+ // appear. Using UnderlineSpan instead though causes the correct behaviour to
+ // happen
+ // (only a
+ // single underline).
SpannableStringBuilder ssb = new SpannableStringBuilder();
ssb.append(t);
@@ -2684,13 +2715,6 @@
ssb.setSpan(new UnderlineSpan(), 0, ssb.length(), Spanned.SPAN_MARK_MARK);
}
- // When letter spacing, align and ellipsize are applied to text, the ellipsized
- // line is indented wrong. This adds the IndentationFixSpan in order to fix
- // the issue.
- if (shouldAttachIndentationFixSpan(text)) {
- attachIndentationFixSpan(ssb, /* layoutForMeasuring= */ null);
- }
-
textView.setText(ssb);
},
posId,
@@ -2713,7 +2737,7 @@
if (overflow.getValue() == TextOverflow.TEXT_OVERFLOW_ELLIPSIZE
&& !text.getText().hasDynamicValue()) {
- adjustMaxLinesForEllipsize(textView, shouldAttachIndentationFixSpan(text));
+ adjustMaxLinesForEllipsize(textView);
}
// Text auto size is not supported for dynamic text.
@@ -2804,78 +2828,6 @@
}
/**
- * Checks whether the {@link IndentationFixSpan} needs to be attached to fix the alignment on
- * text.
- */
- private static boolean shouldAttachIndentationFixSpan(@NonNull Text text) {
- boolean hasLetterSpacing =
- text.hasFontStyle()
- && text.getFontStyle().hasLetterSpacing()
- && text.getFontStyle().getLetterSpacing().getValue() != 0;
- boolean hasEllipsize =
- text.hasOverflow()
- && (text.getOverflow().getValue() == TextOverflow.TEXT_OVERFLOW_ELLIPSIZE
- || text.getOverflow().getValue()
- == TextOverflow.TEXT_OVERFLOW_ELLIPSIZE_END);
- // Since default align is center, we need fix when either alignment is not set or it's set
- // to center.
- boolean isCenterAligned =
- !text.hasMultilineAlignment()
- || text.getMultilineAlignment().getValue()
- == TextAlignment.TEXT_ALIGN_CENTER;
- return hasLetterSpacing && hasEllipsize && isCenterAligned;
- }
-
- /**
- * This fixes that issue by correctly indenting the ellipsized line by translating the canvas on
- * the opposite direction.
- *
- * <p>When letter spacing, center alignment and ellipsize are all set to a TextView, depending
- * on a length of overflow text, the last, ellipsized line starts getting cut of from the
- * start side.
- *
- * <p>It should be applied to a text only when those three attributes are set.
- */
- private static void attachIndentationFixSpan(
- @NonNull SpannableStringBuilder ssb, @Nullable StaticLayout layoutForMeasuring) {
- if (ssb.length() == 0) {
- return;
- }
-
- // Add additional span that accounts for the extra space that TextView adds when ellipsizing
- // text.
- IndentationFixSpan fixSpan =
- layoutForMeasuring == null
- ? new IndentationFixSpan()
- : new IndentationFixSpan(layoutForMeasuring);
- ssb.setSpan(fixSpan, ssb.length() - 1, ssb.length() - 1, /* flags= */ 0);
- }
-
- /**
- * See {@link #attachIndentationFixSpan(SpannableStringBuilder, StaticLayout)}. This method uses
- * {@link StaticLayout} for measurements.
- */
- private static void attachIndentationFixSpan(@NonNull TextView textView) {
- // This is needed to be passed in as the original Layout would have ellipsize on
- // a maxLines and only be updated after it's drawn, so we need to calculate
- // padding based on the StaticLayout.
- StaticLayout layoutForMeasuring =
- StaticLayout.Builder.obtain(
- /* source= */ textView.getText(),
- /* start= */ 0,
- /* end= */ textView.getText().length(),
- /* paint= */ textView.getPaint(),
- /* width= */ textView.getMeasuredWidth())
- .setMaxLines(textView.getMaxLines())
- .setEllipsize(TruncateAt.END)
- .setIncludePad(false)
- .build();
- SpannableStringBuilder ssb = new SpannableStringBuilder(textView.getText());
- attachIndentationFixSpan(ssb, layoutForMeasuring);
- textView.setText(ssb);
- }
-
- /**
* Sorts out what maxLines should be if the text could possibly be truncated before maxLines is
* reached.
*
@@ -2884,17 +2836,12 @@
* different than what TEXT_OVERFLOW_ELLIPSIZE_END does, as that option just ellipsizes the last
* line of text.
*/
- private void adjustMaxLinesForEllipsize(
- @NonNull TextView textView, boolean shouldAttachIndentationFixSpan) {
+ private void adjustMaxLinesForEllipsize(@NonNull TextView textView) {
textView.getViewTreeObserver()
.addOnPreDrawListener(
new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
- if (textView.getText().length() == 0) {
- return true;
- }
-
ViewParent maybeParent = textView.getParent();
if (!(maybeParent instanceof View)) {
Log.d(
@@ -2917,10 +2864,6 @@
// Update only if changed.
if (availableLines < maxMaxLines) {
textView.setMaxLines(availableLines);
-
- if (shouldAttachIndentationFixSpan) {
- attachIndentationFixSpan(textView);
- }
}
// Cancel the current drawing pass.
@@ -3238,7 +3181,7 @@
* to the image view; otherwise returns null to indicate the failure of setting drawable.
*/
@Nullable
- private static Drawable setImageDrawable(
+ private Drawable setImageDrawable(
ImageView imageView, Future<Drawable> drawableFuture, String protoResId) {
try {
return setImageDrawable(imageView, drawableFuture.get(), protoResId);
@@ -3255,8 +3198,10 @@
* null to indicate the failure of setting drawable.
*/
@Nullable
- private static Drawable setImageDrawable(
- ImageView imageView, Drawable drawable, String protoResId) {
+ private Drawable setImageDrawable(ImageView imageView, Drawable drawable, String protoResId) {
+ if (drawable != null) {
+ mInflaterStatsLogger.logDrawableUsage(drawable);
+ }
if (drawable instanceof BitmapDrawable
&& ((BitmapDrawable) drawable).getBitmap().getByteCount()
> DEFAULT_MAX_BITMAP_RAW_SIZE) {
@@ -3386,7 +3331,7 @@
Log.w(
TAG,
"ArcLine length's value_for_layout is not a positive value. Element"
- + " won't be visible.");
+ + " won't be visible.");
}
sizeWrapper.setSweepAngleDegrees(sizeForLayout);
sizedLp.setAngularAlignment(
@@ -4165,8 +4110,9 @@
Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
if (dpProp.hasDynamicValue() && pipelineMaker.isPresent()) {
try {
- pipelineMaker.get().addPipelineFor(dpProp, dpProp.getValue(), posId,
- dynamicValueConsumer);
+ pipelineMaker
+ .get()
+ .addPipelineFor(dpProp, dpProp.getValue(), posId, dynamicValueConsumer);
} catch (RuntimeException ex) {
Log.e(TAG, "Error building pipeline", ex);
staticValueConsumer.accept(dpProp.getValue());
@@ -4229,7 +4175,9 @@
pipelineMaker
.get()
.addPipelineFor(
- floatProp.getDynamicValue(), floatProp.getValue(), posId,
+ floatProp.getDynamicValue(),
+ floatProp.getValue(),
+ posId,
dynamicValueconsumer);
} catch (RuntimeException ex) {
Log.e(TAG, "Error building pipeline", ex);
@@ -4634,7 +4582,7 @@
/** Apply the mutation that was previously computed with {@link #computeMutation}. */
@UiThread
@NonNull
- public ListenableFuture<Void> applyMutation(
+ public ListenableFuture<RenderingArtifact> applyMutation(
@NonNull ViewGroup prevInflatedParent, @NonNull ViewGroupMutation groupMutation) {
RenderedMetadata prevRenderedMetadata = getRenderedMetadata(prevInflatedParent);
if (prevRenderedMetadata != null
@@ -4647,11 +4595,11 @@
}
if (groupMutation.isNoOp()) {
// Nothing to do.
- return immediateVoidFuture();
+ return immediateFuture(RenderingArtifact.create(mInflaterStatsLogger));
}
if (groupMutation.mPipelineMaker.isPresent()) {
- SettableFuture<Void> result = SettableFuture.create();
+ SettableFuture<RenderingArtifact> result = SettableFuture.create();
groupMutation
.mPipelineMaker
.get()
@@ -4661,7 +4609,7 @@
() -> {
try {
applyMutationInternal(prevInflatedParent, groupMutation);
- result.set(null);
+ result.set(RenderingArtifact.create(mInflaterStatsLogger));
} catch (ViewMutationException ex) {
result.setException(ex);
}
@@ -4670,7 +4618,7 @@
} else {
try {
applyMutationInternal(prevInflatedParent, groupMutation);
- return immediateVoidFuture();
+ return immediateFuture(RenderingArtifact.create(mInflaterStatsLogger));
} catch (ViewMutationException ex) {
return immediateFailedFuture(ex);
}
@@ -4679,6 +4627,7 @@
private void applyMutationInternal(
@NonNull ViewGroup prevInflatedParent, @NonNull ViewGroupMutation groupMutation) {
+ mInflaterStatsLogger.logMutationChangedNodes(groupMutation.mInflatedViews.size());
for (InflatedView inflatedView : groupMutation.mInflatedViews) {
String posId = inflatedView.getTag();
if (posId == null) {
@@ -4706,15 +4655,21 @@
}
// Remove the touch delegate to the view to be updated
if (immediateParent.getTouchDelegate() != null) {
- ((TouchDelegateComposite) immediateParent.getTouchDelegate())
- .removeDelegate(viewToUpdate);
+ TouchDelegateComposite delegateComposite =
+ (TouchDelegateComposite) immediateParent.getTouchDelegate();
+ delegateComposite.removeDelegate(viewToUpdate);
// Make sure to remove the touch delegate when the actual clickable view is wrapped,
// for example ImageView inside the RatioViewWrapper
if (viewToUpdate instanceof ViewGroup
&& ((ViewGroup) viewToUpdate).getChildCount() > 0) {
- ((TouchDelegateComposite) immediateParent.getTouchDelegate())
- .removeDelegate(((ViewGroup) viewToUpdate).getChildAt(0));
+ delegateComposite.removeDelegate(((ViewGroup) viewToUpdate).getChildAt(0));
+ }
+
+ // If no more touch delegate left in the composite, remove it completely from the
+ // parent
+ if (delegateComposite.isEmpty()) {
+ immediateParent.setTouchDelegate(null);
}
}
immediateParent.removeViewAt(childIndex);
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
index e6bf97c..6184ac8 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
@@ -83,6 +83,10 @@
mDelegates.remove(delegateView);
}
+ boolean isEmpty() {
+ return mDelegates.isEmpty();
+ }
+
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
boolean eventForwarded = false;
@@ -125,7 +129,7 @@
@Override
@NonNull
public AccessibilityNodeInfo.TouchDelegateInfo getTouchDelegateInfo() {
- if (VERSION.SDK_INT >= VERSION_CODES.Q) {
+ if (VERSION.SDK_INT >= VERSION_CODES.Q && !mDelegates.isEmpty()) {
Map<Region, View> targetMap = new ArrayMap<>(mDelegates.size());
for (Map.Entry<View, DelegateInfo> entry : mDelegates.entrySet()) {
AccessibilityNodeInfo.TouchDelegateInfo info =
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTreeTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTreeTest.java
index 38d616d..a52e426 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTreeTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTreeTest.java
@@ -161,7 +161,7 @@
}
@Test
- public void findAncestor_onlySearchesNodesAboveTheNode() {
+ public void findAncestorValues_onlySearchesNodesAboveTheNode() {
List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
assertThat(mTree.findAncestorsFor(NODE_2_1, nodesOfInterest::contains))
@@ -169,7 +169,15 @@
}
@Test
- public void findAncestor_disjointTree_searchesAllAboveNodes() {
+ public void findAncestorIds_onlySearchesNodesAboveTheNode() {
+ List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
+
+ assertThat(mTree.findAncestorsNodesFor(NODE_2_1, nodesOfInterest::contains))
+ .containsExactly(NODE_2, NODE_ROOT);
+ }
+
+ @Test
+ public void findAncestorValues_disjointTree_searchesAllAboveNodes() {
// Missing NODE_3_1
mTree.addOrReplace(NODE_3_1_1, mNode3Child1Child1);
mTree.addOrReplace(NODE_3_1_1_1, mNode3Child1Child1Child1);
@@ -181,7 +189,19 @@
}
@Test
- public void findAncestor_emptyTree_returnsNothing() {
+ public void findAncestorIds_disjointTree_searchesAllAboveNodes() {
+ // Missing NODE_3_1
+ mTree.addOrReplace(NODE_3_1_1, mNode3Child1Child1);
+ mTree.addOrReplace(NODE_3_1_1_1, mNode3Child1Child1Child1);
+ List<TreeNode> nodesOfInterest =
+ Arrays.asList(mNodeRoot, mNode1, mNode3Child1Child1, mNode3);
+
+ assertThat(mTree.findAncestorsNodesFor(NODE_3_1_1_1, nodesOfInterest::contains))
+ .containsExactly(ROOT_NODE_ID, NODE_3_1_1, NODE_3);
+ }
+
+ @Test
+ public void findAncestorValues_emptyTree_returnsNothing() {
mTree.clear();
List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
@@ -189,11 +209,24 @@
}
@Test
- public void findAncestor_noMatch_returnsNothing() {
+ public void findAncestorIds_emptyTree_returnsNothing() {
+ mTree.clear();
+ List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
+
+ assertThat(mTree.findAncestorsNodesFor(NODE_2_1, nodesOfInterest::contains)).isEmpty();
+ }
+
+ @Test
+ public void findAncestorValues_noMatch_returnsNothing() {
assertThat(mTree.findAncestorsFor(NODE_2_1, treeNode -> false)).isEmpty();
}
@Test
+ public void findAncestorIds_noMatch_returnsNothing() {
+ assertThat(mTree.findAncestorsNodesFor(NODE_2_1, treeNode -> false)).isEmpty();
+ }
+
+ @Test
public void findChildren_onlySearchesBelowTheNode() {
List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinter.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinter.java
index de6cd5a..0f8d2d3 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinter.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinter.java
@@ -16,6 +16,8 @@
package androidx.wear.protolayout.renderer.helper;
+import static androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement.InnerCase.ADAPTER;
+
import androidx.annotation.Nullable;
import androidx.wear.protolayout.proto.FingerprintProto.NodeFingerprint;
import androidx.wear.protolayout.proto.FingerprintProto.TreeFingerprint;
@@ -96,6 +98,7 @@
for (LayoutElementProto.ArcLayoutElement child : getArcChildren(element)) {
addNodeToParent(child, currentFingerprintBuilder);
}
+
NodeFingerprint currentFingerprint = currentFingerprintBuilder.build();
if (parentFingerprintBuilder != null) {
addNodeToParent(currentFingerprint, parentFingerprintBuilder);
@@ -106,12 +109,14 @@
private void addNodeToParent(
LayoutElementProto.ArcLayoutElement element,
NodeFingerprint.Builder parentFingerprintBuilder) {
- addNodeToParent(
+ NodeFingerprint.Builder currentFingerprint =
NodeFingerprint.newBuilder()
.setSelfTypeValue(getSelfTypeFingerprint(element))
- .setSelfPropsValue(getSelfPropsFingerprint(element))
- .build(),
- parentFingerprintBuilder);
+ .setSelfPropsValue(getSelfPropsFingerprint(element));
+ if (element.getInnerCase() == ADAPTER) {
+ addNodeToParent(element.getAdapter().getContent(), currentFingerprint);
+ }
+ addNodeToParent(currentFingerprint.build(), parentFingerprintBuilder);
}
private void addNodeToParent(
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinterTest.java
index 15f0481..b0d7c9c 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinterTest.java
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.wear.protolayout.renderer.helper;
import static androidx.wear.protolayout.renderer.helper.TestDsl.arc;
+import static androidx.wear.protolayout.renderer.helper.TestDsl.arcAdapter;
import static androidx.wear.protolayout.renderer.helper.TestDsl.arcText;
import static androidx.wear.protolayout.renderer.helper.TestDsl.column;
import static androidx.wear.protolayout.renderer.helper.TestDsl.layout;
@@ -43,57 +43,62 @@
TestFingerprinter.getDefault()
.buildLayoutWithFingerprints(referenceLayout().getRoot());
NodeFingerprint root = layout.getFingerprint().getRoot();
-
Set<Integer> selfPropsFingerprints = new HashSet<>();
Set<Integer> childFingerprints = new HashSet<>();
-
// 1
NodeFingerprint node = root;
assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
assertThat(node.getChildNodesCount()).isEqualTo(4);
assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
-
// 1.1
node = root.getChildNodes(0);
assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
assertThat(node.getChildNodesCount()).isEqualTo(2);
assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
-
// 1.1.1
node = root.getChildNodes(0).getChildNodes(0);
assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
assertThat(node.getChildNodesList()).isEmpty();
assertThat(node.getChildNodesValue()).isEqualTo(0);
-
// 1.1.2
node = root.getChildNodes(0).getChildNodes(1);
assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
assertThat(node.getChildNodesList()).isEmpty();
assertThat(node.getChildNodesValue()).isEqualTo(0);
-
// 1.2
node = root.getChildNodes(1);
assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
assertThat(node.getChildNodesCount()).isEqualTo(1);
assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
-
// 1.2.1
node = root.getChildNodes(1).getChildNodes(0);
assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
assertThat(node.getChildNodesList()).isEmpty();
assertThat(node.getChildNodesValue()).isEqualTo(0);
-
// 1.3
node = root.getChildNodes(2);
assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
assertThat(node.getChildNodesList()).isEmpty();
assertThat(node.getChildNodesValue()).isEqualTo(0);
-
// 1.4
node = root.getChildNodes(3);
assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
+ assertThat(node.getChildNodesCount()).isEqualTo(2);
+ assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
+ // 1.4.1
+ node = root.getChildNodes(3).getChildNodes(0);
+ assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
+ assertThat(node.getChildNodesCount()).isEqualTo(0);
+ assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
+ // 1.4.2
+ node = root.getChildNodes(3).getChildNodes(1);
+ assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
assertThat(node.getChildNodesCount()).isEqualTo(1);
assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
+ // 1.4.2.1
+ node = root.getChildNodes(3).getChildNodes(1).getChildNodes(0);
+ assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
+ assertThat(node.getChildNodesCount()).isEqualTo(0);
}
@Test
@@ -102,12 +107,10 @@
TestFingerprinter.getDefault()
.buildLayoutWithFingerprints(referenceLayout().getRoot());
NodeFingerprint refRoot = refLayout.getFingerprint().getRoot();
-
Layout layout =
TestFingerprinter.getDefault()
.buildLayoutWithFingerprints(layoutWithDifferentColumnHeight().getRoot());
NodeFingerprint root = layout.getFingerprint().getRoot();
-
// 1
NodeFingerprint refNode = refRoot;
NodeFingerprint node = root;
@@ -116,7 +119,6 @@
.isNotEqualTo(refNode.getSelfPropsValue()); // Only difference
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.1
refNode = refRoot.getChildNodes(0);
node = root.getChildNodes(0);
@@ -124,7 +126,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.1.1
refNode = refRoot.getChildNodes(0).getChildNodes(0);
node = root.getChildNodes(0).getChildNodes(0);
@@ -132,7 +133,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.1.2
refNode = refRoot.getChildNodes(0).getChildNodes(1);
node = root.getChildNodes(0).getChildNodes(1);
@@ -140,7 +140,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.2
refNode = refRoot.getChildNodes(1);
node = root.getChildNodes(1);
@@ -148,7 +147,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.2.1
refNode = refRoot.getChildNodes(1).getChildNodes(0);
node = root.getChildNodes(1).getChildNodes(0);
@@ -156,7 +154,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.3
refNode = refRoot.getChildNodes(2);
node = root.getChildNodes(2);
@@ -164,7 +161,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.4
refNode = refRoot.getChildNodes(3);
node = root.getChildNodes(3);
@@ -180,12 +176,10 @@
TestFingerprinter.getDefault()
.buildLayoutWithFingerprints(referenceLayout().getRoot());
NodeFingerprint refRoot = refLayout.getFingerprint().getRoot();
-
Layout layout =
TestFingerprinter.getDefault()
.buildLayoutWithFingerprints(layoutWithDifferentText().getRoot());
NodeFingerprint root = layout.getFingerprint().getRoot();
-
// 1
NodeFingerprint refNode = refRoot;
NodeFingerprint node = root;
@@ -193,7 +187,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isNotEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.1
refNode = refRoot.getChildNodes(0);
node = root.getChildNodes(0);
@@ -201,7 +194,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.1.1
refNode = refRoot.getChildNodes(0).getChildNodes(0);
node = root.getChildNodes(0).getChildNodes(0);
@@ -209,7 +201,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.1.2
refNode = refRoot.getChildNodes(0).getChildNodes(1);
node = root.getChildNodes(0).getChildNodes(1);
@@ -217,7 +208,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.2
refNode = refRoot.getChildNodes(1);
node = root.getChildNodes(1);
@@ -225,7 +215,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isNotEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.2.1
refNode = refRoot.getChildNodes(1).getChildNodes(0);
node = root.getChildNodes(1).getChildNodes(0);
@@ -234,7 +223,6 @@
.isNotEqualTo(refNode.getSelfPropsValue()); // Updated text
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.3
refNode = refRoot.getChildNodes(2);
node = root.getChildNodes(2);
@@ -242,7 +230,6 @@
assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
// 1.4
refNode = refRoot.getChildNodes(3);
node = root.getChildNodes(3);
@@ -273,8 +260,10 @@
text("blah blah"), // 1.3
arc( // 1.4
props -> props.anchorAngleDegrees = 15, // arc props
- arcText("arctext") // 1.4.1
- )));
+ arcText("arctext"), // 1.4.1
+ arcAdapter( // 1.4.2
+ text("text") // 1.4.2.1
+ ))));
}
private static Layout layoutWithDifferentColumnHeight() {
@@ -298,8 +287,10 @@
text("blah blah"), // 1.3
arc( // 1.4
props -> props.anchorAngleDegrees = 15, // arc props
- arcText("arctext") // 1.4.1
- )));
+ arcText("arctext"), // 1.4.1
+ arcAdapter( // 1.4.2
+ text("text") // 1.4.2.1
+ ))));
}
private static Layout layoutWithDifferentText() {
@@ -323,7 +314,9 @@
text("blah blah"), // 1.3
arc( // 1.4
props -> props.anchorAngleDegrees = 15, // arc props
- arcText("arctext") // 1.4.1
- )));
+ arcText("arctext"), // 1.4.1
+ arcAdapter( // 1.4.2
+ text("text") // 1.4.2.1
+ ))));
}
}
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
index 6db2dfb..d095d58 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
@@ -53,6 +53,7 @@
import androidx.wear.protolayout.expression.pipeline.StateStore;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.ResourceProto.Resources;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
import androidx.wear.protolayout.renderer.helper.TestDsl.LayoutNode;
import androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config;
@@ -97,8 +98,8 @@
@Test
public void adaptiveUpdateRatesDisabled_attach_reinflatesCompletely() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -108,7 +109,7 @@
assertThat(layout1).hasSize(1);
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -120,8 +121,8 @@
@Test
public void adaptiveUpdateRatesEnabled_attach_appliesDiffOnly() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -131,7 +132,7 @@
assertThat(layout1).hasSize(1);
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -144,8 +145,8 @@
@Test
public void reattach_usesCachedLayoutForDiffUpdate() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -156,7 +157,7 @@
mInstanceUnderTest.detach(mRootContainer);
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -172,8 +173,8 @@
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
// First one that does the full layout update.
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -184,7 +185,7 @@
// Second one that applies mutation only.
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
// Detach so it can't apply update.
mInstanceUnderTest.detach(mRootContainer);
@@ -200,9 +201,8 @@
// Render the first layout.
Layout layout1 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT2)));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
- layout1, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -227,9 +227,7 @@
// not changed part of the layout was also changed in inflated View.
Layout layout2 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT3)));
- result =
- mInstanceUnderTest.renderAndAttach(
- layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
// Make sure future is computing result.
assertThat(result.isDone()).isFalse();
@@ -252,8 +250,8 @@
// Render the first layout.
Layout layout1 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT2)));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -263,7 +261,7 @@
assertThat(findViewsWithText(mRootContainer, TEXT2)).hasSize(1);
Layout layout2 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT3)));
- result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
// Make sure future is computing result.
assertThat(result.isDone()).isFalse();
shadowOf(Looper.getMainLooper()).idle();
@@ -279,13 +277,13 @@
public void adaptiveUpdateRatesEnabled_ongoingRendering_skipsPreviousLayout() {
FrameLayout container = new FrameLayout(mApplicationContext);
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result1 =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result1 =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container);
assertThat(result1.isDone()).isFalse();
- ListenableFuture<Void> result2 =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result2 =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, container);
shadowOf(Looper.getMainLooper()).idle();
@@ -301,14 +299,14 @@
FrameLayout container1 = new FrameLayout(mApplicationContext);
FrameLayout container2 = new FrameLayout(mApplicationContext);
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container1);
assertThrows(
IllegalStateException.class,
() ->
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container2));
shadowOf(Looper.getMainLooper()).idle();
@@ -321,12 +319,14 @@
FrameLayout container1 = new FrameLayout(mApplicationContext);
FrameLayout container2 = new FrameLayout(mApplicationContext);
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result1 =
- mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container1);
+ ListenableFuture<RenderingArtifact> result1 =
+ mInstanceUnderTest.renderLayoutAndAttach(
+ layout(text(TEXT1)), RESOURCES, container1);
mInstanceUnderTest.detach(container1);
- ListenableFuture<Void> result2 =
- mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container2);
+ ListenableFuture<RenderingArtifact> result2 =
+ mInstanceUnderTest.renderLayoutAndAttach(
+ layout(text(TEXT1)), RESOURCES, container2);
shadowOf(Looper.getMainLooper()).idle();
assertThat(result1.isCancelled()).isTrue();
@@ -341,13 +341,13 @@
throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
- result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -360,14 +360,14 @@
Layout layout1 = layout(text(TEXT1));
Layout layout2 = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
// Make sure we have an UnchangedRenderResult
- result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -377,7 +377,7 @@
assertThat(findViewsWithText(mRootContainer, TEXT1)).isEmpty();
shadowOf(Looper.getMainLooper()).idle();
- result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
assertThat(result.isDone()).isTrue();
assertNoException(result);
@@ -388,8 +388,8 @@
public void fullInflationResultCanBeReused() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
Layout layout = layout(text(TEXT1));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -397,7 +397,7 @@
ListenableFuture<?> renderFuture = mInstanceUnderTest.mRenderFuture;
mInstanceUnderTest.detach(mRootContainer);
- result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -409,15 +409,15 @@
throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
List<View> textViews1 = findViewsWithText(mRootContainer, TEXT1);
assertThat(textViews1).hasSize(1);
mInstanceUnderTest.close();
- result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
assertThat(shadowOf(Looper.getMainLooper()).isIdle()).isFalse();
shadowOf(Looper.getMainLooper()).idle();
@@ -431,8 +431,8 @@
public void detach_clearsHostView() throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
@@ -449,14 +449,14 @@
Layout layout2 = layout(text(TEXT1));
Resources resources2 = Resources.newBuilder().setVersion("2").build();
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
- result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, resources2, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -472,15 +472,15 @@
Layout layout2 = layout(text(TEXT1));
Resources resources2 = Resources.newBuilder().setVersion("1").build();
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
mInstanceUnderTest.invalidateCache();
- result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, resources2, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -490,13 +490,28 @@
}
@Test
+ public void invalidateCache_ongoingInflation_oldInflationGetsCancelled() throws Exception {
+ Layout layout1 = layout(text(TEXT1));
+ Resources resources1 = Resources.newBuilder().setVersion("1").build();
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
+
+ mInstanceUnderTest.invalidateCache();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(result.isCancelled()).isTrue();
+ assertThat(findViewsWithText(mRootContainer, TEXT1)).isEmpty();
+ }
+
+ @Test
public void adaptiveUpdateRatesEnabled_rootElementdiff_keepsElementCentered() throws Exception {
int dimension = 50;
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
// Full inflation.
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(
column(
props -> {
@@ -518,7 +533,7 @@
// Diff update only for the root element.
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(
column(
props -> {
@@ -546,8 +561,8 @@
public void close_clearsHostView() throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
@@ -562,7 +577,9 @@
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
assertThrows(
ExecutionException.class,
- () -> renderAndAttachLayout(layout(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH + 1))));
+ () ->
+ renderLayoutAndAttachLayout(
+ layout(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH + 1))));
}
@Test
@@ -573,8 +590,8 @@
for (int i = 0; i < children.length; i++) {
children[i] = recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1);
}
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
// MAX_LAYOUT_ELEMENT_DEPTH branches of depth MAX_LAYOUT_ELEMENT_DEPTH - 1.
// Total depth is MAX_LAYOUT_ELEMENT_DEPTH (if we count the head).
layout(box(children)), RESOURCES, mRootContainer);
@@ -591,7 +608,7 @@
assertThrows(
ExecutionException.class,
() ->
- renderAndAttachLayout(
+ renderLayoutAndAttachLayout(
// Total number of views is = MAX_LAYOUT_ELEMENT_DEPTH + 1 (span
// text)
layout(
@@ -599,8 +616,8 @@
MAX_LAYOUT_ELEMENT_DEPTH,
spannable(spanText("Hello"))))));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
// Total number of views is = (MAX_LAYOUT_ELEMENT_DEPTH -1) + 1 (span text)
layout(
recursiveBox(
@@ -620,12 +637,12 @@
assertThrows(
ExecutionException.class,
() ->
- renderAndAttachLayout(
+ renderLayoutAndAttachLayout(
// Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH)
layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH))))));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
// Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH - 1)
// = MAX_LAYOUT_ELEMENT_DEPTH
layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1)))),
@@ -637,9 +654,9 @@
assertThat(mRootContainer.getChildCount()).isEqualTo(1);
}
- private void renderAndAttachLayout(Layout layout) throws Exception {
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ private void renderLayoutAndAttachLayout(Layout layout) throws Exception {
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
}
@@ -690,7 +707,8 @@
return views;
}
- private static void assertNoException(ListenableFuture<Void> result) throws Exception {
+ private static void assertNoException(ListenableFuture<RenderingArtifact> result)
+ throws Exception {
// Assert that result hasn't thrown exception.
result.get();
}
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpanTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpanTest.java
deleted file mode 100644
index e795c5c..0000000
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpanTest.java
+++ /dev/null
@@ -1,324 +0,0 @@
-/*
- * Copyright 2024 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.
- */
-
-package androidx.wear.protolayout.renderer.inflater;
-
-import static androidx.wear.protolayout.renderer.inflater.IndentationFixSpan.ELLIPSIS_CHAR;
-import static androidx.wear.protolayout.renderer.inflater.IndentationFixSpan.calculatePadding;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.text.Layout;
-import android.text.Layout.Alignment;
-import android.text.StaticLayout;
-import android.text.TextPaint;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.google.common.truth.Expect;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Objects;
-
-/**
- * Tests that the actual padding is correctly calculated. The translation of canvas is tested with
- * screenshot tests.
- */
-@RunWith(AndroidJUnit4.class)
-public class IndentationFixSpanTest {
-
- private static final String TEST_TEXT = "Test";
- private static final int DEFAULT_ELLIPSIZE_START = 100;
- private static final TestPaint PAINT = new TestPaint();
-
- @Rule public final Expect expect = Expect.create();
-
- @Test
- public void test_givenLayout_correctObjectIsUsed() {
- StaticLayout layout = mock(StaticLayout.class);
- IndentationFixSpan span = new IndentationFixSpan(layout);
- Layout givenLayout = mock(Layout.class);
-
- span.drawLeadingMargin(
- mock(Canvas.class),
- mock(Paint.class),
- /* x= */ 0,
- /* dir= */ 0,
- /* top= */ 0,
- /* baseline= */ 0,
- /* bottom= */ 0,
- "Text",
- /* start= */ 0,
- /* end= */ 0,
- false,
- givenLayout);
-
- verifyNoInteractions(givenLayout);
-
- verify(layout).getLineCount();
- verify(layout).getLineForOffset(/* offset= */ 0);
- }
-
- @Test
- public void test_calculatedPadding_onRtl_centerAlign_correctValue() {
- TestLayoutRtl layout =
- new TestLayoutRtl(
- TEST_TEXT,
- /* width= */ 288,
- Alignment.ALIGN_CENTER,
- /* mainLineIndex= */ 2);
-
- // On ellipsized line there is padding.
- expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(-8.5f);
- }
-
- @Test
- public void test_calculatedPadding_onRtl_normalAlign_correctValue() {
- TestLayoutRtl layout =
- new TestLayoutRtl(
- TEST_TEXT,
- /* width= */ 288,
- Alignment.ALIGN_NORMAL,
- /* mainLineIndex= */ 2);
-
- // On ellipsized line there is padding.
- expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(-9f);
- }
-
- @Test
- public void test_calculatedPadding_onLtr_centerAlign_correctValue() {
- TestLayoutLtr layout =
- new TestLayoutLtr(
- TEST_TEXT,
- /* width= */ 300,
- Alignment.ALIGN_CENTER,
- /* mainLineIndex= */ 2);
-
- // On ellipsized line there is padding.
- expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(13f);
- }
-
- @Test
- public void test_calculatedPadding_onLtr_normalAlign_correctValue() {
- TestLayoutLtr layout =
- new TestLayoutLtr(
- TEST_TEXT,
- /* width= */ 300,
- Alignment.ALIGN_NORMAL,
- /* mainLineIndex= */ 2);
-
- // On ellipsized line there is padding.
- expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(19f);
- }
-
- @Test
- public void test_calculatePadding_lastLineNotEllipsize_returnsZero() {
- // Number of lines so that notEllipsizedLineIndex is the last one.
- TestLayoutLtr layout =
- new TestLayoutLtr(
- TEST_TEXT,
- /* width= */ 300,
- Alignment.ALIGN_CENTER,
- /* mainLineIndex= */ 2);
- // But not ellipsized.
- layout.removeEllipsisCount();
-
- // On not ellipsized line there is no padding.
- expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(0);
- }
-
- @Test
- public void test_calculatePadding_notLastLine_returnsZero() {
- // Number of lines so that notEllipsizedLineIndex is the last one.
- TestLayoutLtr layout =
- new TestLayoutLtr(
- TEST_TEXT,
- /* width= */ 300,
- Alignment.ALIGN_CENTER,
- /* mainLineIndex= */ 2);
- // Number of lines so that lineIndex is NOT the last one.
- layout.increaseLineCount();
-
- // On not last line there is no padding.
- expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(0);
- }
-
- private static class TestPaint extends TextPaint {
-
- @Override
- public float measureText(String text) {
- if (Objects.equals(text, ELLIPSIS_CHAR)) {
- return 23f;
- }
- return super.measureText(text);
- }
- }
-
- /**
- * Test only implementation of {@link Layout} with numbers so we can test padding correctly.
- */
- private abstract static class TestLayout extends Layout {
-
- private static final int DEFAULT_ELLIPSIS_COUNT = 3;
- protected final int mMainLineIndex;
-
- // Overridable values for the mainLineIndex.
- private int mLineCount;
- private int mEllipsisCount = DEFAULT_ELLIPSIS_COUNT;
-
- protected TestLayout(CharSequence text, int width, Alignment align, int mainLineIndex) {
- super(text, PAINT, width, align, /* spacingMult= */ 0, /* spacingAdd= */ 0);
- this.mMainLineIndex = mainLineIndex;
- mLineCount = mainLineIndex + 1;
- }
-
- void increaseLineCount() {
- mLineCount = mLineCount + 3;
- }
-
- void removeEllipsisCount() {
- this.mEllipsisCount = 0;
- }
-
- @Override
- public int getLineCount() {
- return mLineCount;
- }
-
- @Override
- public int getLineTop(int line) {
- // N/A
- return 0;
- }
-
- @Override
- public int getLineDescent(int line) {
- // N/A
- return 0;
- }
-
- @Override
- public int getLineStart(int line) {
- return 0;
- }
-
- @Override
- public boolean getLineContainsTab(int line) {
- // N/A
- return false;
- }
-
- @Override
- public Directions getLineDirections(int line) {
- // N/A
- return null;
- }
-
- @Override
- public int getTopPadding() {
- // N/A
- return 0;
- }
-
- @Override
- public int getBottomPadding() {
- // N/A
- return 0;
- }
-
- @Override
- public int getEllipsisCount(int line) {
- return line == mMainLineIndex ? /* non zero */ mEllipsisCount : 0;
- }
-
- @Override
- public int getLineForOffset(int offset) {
- return offset == DEFAULT_ELLIPSIZE_START ? mMainLineIndex : 0;
- }
- }
-
- /**
- * Specific implementation of {@link Layout} that returns numbers for LTR testing of padding.
- */
- private static class TestLayoutLtr extends TestLayout {
-
- protected TestLayoutLtr(CharSequence text, int width, Alignment align, int mainLineIndex) {
- super(text, width, align, mainLineIndex);
- }
-
- @Override
- public float getPrimaryHorizontal(int offset) {
- return 258f;
- }
-
- @Override
- public float getLineLeft(int line) {
- return line == mMainLineIndex ? -7f : super.getLineLeft(line);
- }
-
- @Override
- public int getEllipsisStart(int line) {
- return 20;
- }
-
- @Override
- public int getParagraphDirection(int line) {
- return Layout.DIR_LEFT_TO_RIGHT;
- }
- }
-
- /**
- * Specific implementation of {@link Layout} that returns numbers for RTL testing of padding.
- */
- private static class TestLayoutRtl extends TestLayout {
-
- protected TestLayoutRtl(CharSequence text, int width, Alignment align, int mainLineIndex) {
- super(text, width, align, mainLineIndex);
- }
-
- @Override
- public float getPrimaryHorizontal(int offset) {
- return 32f;
- }
-
- @Override
- public float getLineLeft(int line) {
- return line == mMainLineIndex ? -7f : super.getLineLeft(line);
- }
-
- @Override
- public int getEllipsisStart(int line) {
- return 20;
- }
-
- @Override
- public int getParagraphDirection(int line) {
- return Layout.DIR_RIGHT_TO_LEFT;
- }
-
- @Override
- public float getLineRight(int line) {
- return line == mMainLineIndex ? 296f : super.getLineRight(line);
- }
- }
-}
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index da07985..1a46140 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -83,7 +83,6 @@
import androidx.core.content.ContextCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
import androidx.wear.protolayout.expression.AppDataKey;
import androidx.wear.protolayout.expression.DynamicBuilders;
import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
@@ -203,6 +202,8 @@
import androidx.wear.protolayout.proto.TypesProto.StringProp;
import androidx.wear.protolayout.protobuf.ByteString;
import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
import androidx.wear.protolayout.renderer.helper.TestFingerprinter;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
@@ -222,6 +223,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowChoreographer;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowPackageManager;
@@ -543,37 +545,41 @@
int width = 10;
int height = 12;
byte[] payload = "Hello World".getBytes(UTF_8);
+ LayoutElement extension =
+ LayoutElement.newBuilder()
+ .setExtension(
+ ExtensionLayoutElement.newBuilder()
+ .setExtensionId("foo")
+ .setPayload(ByteString.copyFrom(payload))
+ .setWidth(
+ ExtensionDimension.newBuilder()
+ .setLinearDimension(dp(width))
+ .build())
+ .setHeight(
+ ExtensionDimension.newBuilder()
+ .setLinearDimension(dp(height))
+ .build()))
+ .build();
LayoutElement root =
LayoutElement.newBuilder()
- .setBox(
- Box.newBuilder()
- // Outer box's width and height left at default value of "wrap"
- .addContents(
- LayoutElement.newBuilder()
- .setExtension(
- ExtensionLayoutElement.newBuilder()
- .setExtensionId("foo")
- .setPayload(ByteString.copyFrom(payload))
- .setWidth(
- ExtensionDimension.newBuilder()
- .setLinearDimension(dp(width))
- .build())
- .setHeight(
- ExtensionDimension.newBuilder()
- .setLinearDimension(dp(height))
- .build()))))
+ .setBox(
+ Box.newBuilder()
+ // Outer box's width and height left at default value of
+ // "wrap"
+ .addContents(extension))
.build();
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(fingerprintedLayout(root))
- .setExtensionViewProvider(
- (extensionPayload, id) -> {
- TextView returnedView = new TextView(getApplicationContext());
- returnedView.setText("testing");
+ newRendererConfigBuilder(fingerprintedLayout(root))
+ .setExtensionViewProvider(
+ (extensionPayload, id) -> {
+ TextView returnedView =
+ new TextView(getApplicationContext());
+ returnedView.setText("testing");
- return returnedView;
- }))
+ return returnedView;
+ }))
.inflate();
// Check that the outer box is displayed and it has a child.
@@ -976,6 +982,23 @@
// A column with a row (Spacer + Spacer) and Spacer, everything has weighted expand
// dimension.
+
+ Row rowWithSpacers =
+ Row.newBuilder()
+ .setWidth(expand())
+ .setHeight(
+ ContainerDimension.newBuilder()
+ .setExpandedDimension(expandWithWeight(heightWeight1))
+ .build())
+ .addContents(
+ LayoutElement.newBuilder()
+ .setSpacer(
+ buildExpandedSpacer(widthWeight1, DEFAULT_WEIGHT)))
+ .addContents(
+ LayoutElement.newBuilder()
+ .setSpacer(
+ buildExpandedSpacer(widthWeight2, DEFAULT_WEIGHT)))
+ .build();
LayoutElement root =
LayoutElement.newBuilder()
.setColumn(
@@ -983,26 +1006,13 @@
.setWidth(expand())
.setHeight(expand())
.addContents(
- LayoutElement.newBuilder()
- .setRow(
- Row.newBuilder()
- .setWidth(expand())
- .setHeight(
- ContainerDimension.newBuilder()
- .setExpandedDimension(expandWithWeight(heightWeight1))
- .build())
- .addContents(
- LayoutElement.newBuilder()
- .setSpacer(
- buildExpandedSpacer(widthWeight1, DEFAULT_WEIGHT)))
- .addContents(
- LayoutElement.newBuilder()
- .setSpacer(
- buildExpandedSpacer(
- widthWeight2, DEFAULT_WEIGHT)))))
+ LayoutElement.newBuilder().setRow(rowWithSpacers))
.addContents(
LayoutElement.newBuilder()
- .setSpacer(buildExpandedSpacer(DEFAULT_WEIGHT, heightWeight2)))
+ .setSpacer(
+ buildExpandedSpacer(
+ DEFAULT_WEIGHT,
+ heightWeight2)))
.build())
.build();
@@ -1034,9 +1044,9 @@
.setExpandedDimension(
ExpandedDimensionProp
.getDefaultInstance()))
- .setWidth(SpacerDimension
- .newBuilder()
- .setLinearDimension(dp(width))))
+ .setWidth(
+ SpacerDimension.newBuilder()
+ .setLinearDimension(dp(width))))
.build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
@@ -1543,17 +1553,17 @@
ContainerDimension.newBuilder().setLinearDimension(dp(childSize)).build();
LayoutElement childBox =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(childBoxSize)
- .setHeight(childBoxSize)
- .setModifiers(
- Modifiers.newBuilder()
- .setClickable(
- Clickable.newBuilder()
- .setId("foo")
- .setOnClick(
- action))))
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(childBoxSize)
+ .setHeight(childBoxSize)
+ .setModifiers(
+ Modifiers.newBuilder()
+ .setClickable(
+ Clickable.newBuilder()
+ .setId("foo")
+ .setOnClick(action))))
.build();
LayoutElement root =
@@ -1563,13 +1573,14 @@
.setWidth(parentBoxSize)
.setHeight(parentBoxSize)
.addContents(childBox))
- .build();
+ .build();
State.Builder receivedState = State.newBuilder();
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
- .setLoadActionListener(receivedState::mergeFrom))
+ newRendererConfigBuilder(
+ fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(receivedState::mergeFrom))
.inflate();
shadowOf(Looper.getMainLooper()).idle();
@@ -1627,8 +1638,7 @@
.setWidth(
SpacerDimension.newBuilder()
.setLinearDimension(dp(spacerSize))
- .build()
- ));
+ .build()));
// |--clickable area child box 1 (5 - 35)--|
// |---clickable area child box 2 (30-60)--|
@@ -1647,8 +1657,7 @@
dp(clickTargetSize))
.setMinimumClickableHeight(
dp(clickTargetSize))
- .setOnClick(
- action)
+ .setOnClick(action)
.setId("foo1"))))
.build();
@@ -1666,8 +1675,7 @@
dp(clickTargetSize))
.setMinimumClickableHeight(
dp(clickTargetSize))
- .setOnClick(
- action)
+ .setOnClick(action)
.setId("foo2"))))
.build();
@@ -1692,8 +1700,9 @@
State.Builder receivedState = State.newBuilder();
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
- .setLoadActionListener(receivedState::mergeFrom))
+ newRendererConfigBuilder(
+ fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(receivedState::mergeFrom))
.inflate();
ShadowLooper.runUiThreadTasks();
@@ -1812,8 +1821,8 @@
// Compute the mutation
ViewGroupMutation mutation =
- renderer.computeMutation(getRenderedMetadata(rootLayout),
- fingerprintedLayout(root2));
+ renderer.computeMutation(
+ getRenderedMetadata(rootLayout), fingerprintedLayout(root2));
assertThat(mutation).isNotNull();
assertThat(mutation.isNoOp()).isFalse();
@@ -1848,8 +1857,9 @@
.setSpacer(
Spacer.newBuilder()
.setWidth(
- SpacerDimension.newBuilder().setLinearDimension(
- dp(spacerSize)).build()));
+ SpacerDimension.newBuilder()
+ .setLinearDimension(dp(spacerSize))
+ .build()));
int parentHeight = 45;
int parentWidth = 125;
@@ -1916,13 +1926,13 @@
// Compute the mutation
ViewGroupMutation mutation =
- renderer.computeMutation(getRenderedMetadata(rootLayout),
- fingerprintedLayout(root2));
+ renderer.computeMutation(
+ getRenderedMetadata(rootLayout), fingerprintedLayout(root2));
assertThat(mutation).isNotNull();
assertThat(mutation.isNoOp()).isFalse();
// Apply the mutation
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(rootLayout, mutation);
shadowOf(getMainLooper()).idle();
try {
@@ -1958,6 +1968,108 @@
}
@Test
+ @Config(minSdk = VERSION_CODES.Q)
+ public void inflateThenMutate_withClickableSizeChange_clickableModifier_extendClickTargetSize()
+ {
+ Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build();
+ int parentSize = 50;
+ ContainerDimension parentBoxSize =
+ ContainerDimension.newBuilder().setLinearDimension(dp(parentSize)).build();
+ ContainerDimension childBoxSize =
+ ContainerDimension.newBuilder().setLinearDimension(dp(parentSize / 2f)).build();
+
+ Modifiers testModifiers1 =
+ Modifiers.newBuilder()
+ .setClickable(Clickable.newBuilder().setOnClick(action).setId("foo1"))
+ .build();
+
+ // Child box has a size smaller than the minimum clickable size, touch delegation is
+ // required.
+ LayoutElement root =
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(parentBoxSize)
+ .setHeight(parentBoxSize)
+ .addContents(
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(childBoxSize)
+ .setHeight(childBoxSize)
+ .setModifiers(
+ testModifiers1))))
+ .build();
+
+ State.Builder receivedState = State.newBuilder();
+ Renderer renderer =
+ renderer(
+ newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(receivedState::mergeFrom));
+ FrameLayout rootLayout = renderer.inflate();
+ ViewGroup parent = (ViewGroup) rootLayout.getChildAt(0);
+ // Confirm the touch delegation has happened.
+ assertThat(parent.getTouchDelegate()).isNotNull();
+ // Dispatch a click event to the parent View within the expanded clickable area;
+ // it should trigger the LoadAction...
+ receivedState.clearLastClickableId();
+ dispatchTouchEvent(parent, 5, 5);
+ expect.that(receivedState.getLastClickableId()).isEqualTo("foo1");
+
+ // Produce a new layout with child box specifies its minimum clickable size, NO touch
+ // delegation is required.
+ Modifiers testModifiers2 =
+ Modifiers.newBuilder()
+ .setClickable(
+ Clickable.newBuilder()
+ .setOnClick(action)
+ .setId("foo2")
+ .setMinimumClickableWidth(dp(parentSize / 2f))
+ .setMinimumClickableHeight(dp(parentSize / 2f)))
+ .build();
+ LayoutElement root2 =
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(parentBoxSize)
+ .setHeight(parentBoxSize)
+ .addContents(
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(childBoxSize)
+ .setHeight(childBoxSize)
+ .setModifiers(
+ testModifiers2))))
+ .build();
+
+ // Compute the mutation
+ ViewGroupMutation mutation =
+ renderer.computeMutation(getRenderedMetadata(rootLayout),
+ fingerprintedLayout(root2));
+ assertThat(mutation).isNotNull();
+ assertThat(mutation.isNoOp()).isFalse();
+
+ // Apply the mutation
+ boolean mutationResult = renderer.applyMutation(rootLayout, mutation);
+ assertThat(mutationResult).isTrue();
+
+ // Verify that the parent removed the touch delegation.
+ // Keep an empty touch delegate composite will lead to failure when calling
+ // {@link TouchDelegateComposite#getTouchDelegateInfo}
+ assertThat(parent.getTouchDelegate()).isNull();
+
+ // Dispatch a click event to the parent View within the expanded clickable area;
+ // it should no longer trigger the LoadAction.
+ receivedState.clearLastClickableId();
+ dispatchTouchEvent(parent, 5, 5);
+ expect.that(receivedState.getLastClickableId()).isEmpty();
+ View box = parent.getChildAt(0);
+ dispatchTouchEvent(box, 1, 1);
+ expect.that(receivedState.getLastClickableId()).isEqualTo("foo2");
+ }
+
+ @Test
public void inflate_clickable_withoutRippleEffect_rippleDrawableNotAdded() throws IOException {
final String textContentsWithRipple = "clickable with ripple";
final String textContentsWithoutRipple = "clickable without ripple";
@@ -1993,15 +2105,15 @@
FrameLayout rootLayout =
renderer(
- fingerprintedLayout(
- LayoutElement.newBuilder()
- .setColumn(
- Column.newBuilder()
- .addContents(textElementWithRipple)
- .addContents(
- textElementWithoutRipple)
- .build())
- .build()))
+ fingerprintedLayout(
+ LayoutElement.newBuilder()
+ .setColumn(
+ Column.newBuilder()
+ .addContents(textElementWithRipple)
+ .addContents(
+ textElementWithoutRipple)
+ .build())
+ .build()))
.inflate();
// Column
@@ -2025,6 +2137,49 @@
}
@Test
+ public void inflate_hiddenModifier_inhibitsClicks() {
+ final String textContents = "I am a clickable";
+
+ Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build();
+
+ LayoutElement root =
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .addContents(
+ LayoutElement.newBuilder()
+ .setText(
+ createTextWithVisibility(
+ textContents,
+ "back",
+ action,
+ true)))
+ .addContents(
+ LayoutElement.newBuilder()
+ .setText(
+ createTextWithVisibility(
+ textContents,
+ "front",
+ action,
+ false))))
+ .build();
+
+ State.Builder receivedState = State.newBuilder();
+ FrameLayout rootLayout =
+ renderer(
+ newRendererConfigBuilder(
+ fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(receivedState::mergeFrom))
+ .inflate();
+
+ // Try to tap the stacked clickables.
+ dispatchTouchEvent(rootLayout, 5f, 5f);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ expect.that(receivedState.getLastClickableId()).isEqualTo("back");
+ }
+
+ @Test
public void inflate_arc_withLineDrawnWithArcTo() {
LayoutElement root =
LayoutElement.newBuilder()
@@ -2151,8 +2306,8 @@
@Test
public void inflate_arc_withText_autoSize_notSet() {
int lastSize = 12;
- FontStyle.Builder style = FontStyle.newBuilder()
- .addAllSize(buildSizesList(new int[]{10, 20, lastSize}));
+ FontStyle.Builder style =
+ FontStyle.newBuilder().addAllSize(buildSizesList(new int[] {10, 20, lastSize}));
LayoutElement root =
LayoutElement.newBuilder()
.setArc(
@@ -2889,12 +3044,13 @@
.setFontStyle(FontStyle.newBuilder().addSize(sp(16)))
.setMaxLines(Int32Prop.newBuilder().setValue(6))
.setOverflow(
- TextOverflowProp.newBuilder().setValue(
- TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+ TextOverflowProp.newBuilder()
+ .setValue(TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
Layout layout1 =
fingerprintedLayout(
LayoutElement.newBuilder()
- .setBox(buildFixedSizeBoxWIthText(text1)).build());
+ .setBox(buildFixedSizeBoxWIthText(text1))
+ .build());
Text.Builder text2 =
Text.newBuilder()
@@ -2904,18 +3060,19 @@
.setFontStyle(FontStyle.newBuilder().addSize(sp(4)))
.setMaxLines(Int32Prop.newBuilder().setValue(6))
.setOverflow(
- TextOverflowProp.newBuilder().setValue(
- TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+ TextOverflowProp.newBuilder()
+ .setValue(TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
Layout layout2 =
fingerprintedLayout(
LayoutElement.newBuilder()
- .setBox(buildFixedSizeBoxWIthText(text2)).build());
+ .setBox(buildFixedSizeBoxWIthText(text2))
+ .build());
// Initial layout.
Renderer renderer = renderer(layout1);
ViewGroup inflatedViewParent = renderer.inflate();
- TextView textView1 = (TextView) ((ViewGroup) inflatedViewParent
- .getChildAt(0)).getChildAt(0);
+ TextView textView1 =
+ (TextView) ((ViewGroup) inflatedViewParent.getChildAt(0)).getChildAt(0);
// Apply the mutation.
ViewGroupMutation mutation =
@@ -2926,8 +3083,8 @@
assertThat(mutationResult).isTrue();
// This contains layout after the mutation.
- TextView textView2 = (TextView) ((ViewGroup) inflatedViewParent
- .getChildAt(0)).getChildAt(0);
+ TextView textView2 =
+ (TextView) ((ViewGroup) inflatedViewParent.getChildAt(0)).getChildAt(0);
expect.that(textView1.getEllipsize()).isEqualTo(TruncateAt.END);
expect.that(textView1.getMaxLines()).isEqualTo(2);
@@ -3028,7 +3185,7 @@
@Test
public void inflate_textView_autosize_set() {
String text = "Test text";
- int[] presetSizes = new int[]{12, 20, 10};
+ int[] presetSizes = new int[] {12, 20, 10};
List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes);
LayoutElement textElement =
@@ -3036,16 +3193,16 @@
.setText(
Text.newBuilder()
.setText(string(text))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3076,16 +3233,16 @@
Text.newBuilder()
.setText(string(text))
.setMaxLines(Int32Prop.newBuilder().setValue(4))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3099,23 +3256,23 @@
public void inflate_textView_autosize_notSet() {
String text = "Test text";
int size = 24;
- List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{size});
+ List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {size});
LayoutElement textElement =
LayoutElement.newBuilder()
.setText(
Text.newBuilder()
.setText(string(text))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3129,23 +3286,23 @@
public void inflate_textView_autosize_setDynamic_noop() {
String text = "Test text";
int lastSize = 24;
- List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{10, 30, lastSize});
+ List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {10, 30, lastSize});
LayoutElement textElement =
LayoutElement.newBuilder()
.setText(
Text.newBuilder()
.setText(dynamicString(text))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ArrayList<View> textChildren = new ArrayList<>();
@@ -3159,23 +3316,23 @@
@Test
public void inflate_textView_autosize_wrongSizes_noop() {
String text = "Test text";
- List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{0, -2, 0});
+ List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {0, -2, 0});
LayoutElement textElement =
LayoutElement.newBuilder()
.setText(
Text.newBuilder()
.setText(string(text))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ArrayList<View> textChildren = new ArrayList<>();
@@ -3219,8 +3376,8 @@
public void inflate_spantext_ignoresMultipleSizes() {
String text = "Test text";
int firstSize = 12;
- FontStyle.Builder style = FontStyle.newBuilder()
- .addAllSize(buildSizesList(new int[]{firstSize, 10, 20}));
+ FontStyle.Builder style =
+ FontStyle.newBuilder().addAllSize(buildSizesList(new int[] {firstSize, 10, 20}));
LayoutElement root =
LayoutElement.newBuilder()
.setSpannable(
@@ -4780,7 +4937,7 @@
boolean applyMutation(ViewGroup parent, ViewGroupMutation mutation) {
try {
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
mRenderer.applyMutation(parent, mutation);
shadowOf(Looper.getMainLooper()).idle();
applyMutationFuture.get();
@@ -5000,33 +5157,25 @@
LayoutElement image = buildImage(protoResId, 30, 30);
-
- BoolProp.Builder stateBoolPropBuilder = BoolProp
- .newBuilder()
- .setValue(
- true)
- .setDynamicValue(
- DynamicBool
- .newBuilder()
- .setStateSource(
- StateBoolSource
- .newBuilder()
- .setSourceKey(
- boolKey)));
- LayoutElement.Builder boxBuilder = LayoutElement.newBuilder()
- .setBox(
- Box.newBuilder()
- .addContents(image)
- .setModifiers(
- Modifiers
- .newBuilder()
- .setHidden(stateBoolPropBuilder)));
+ BoolProp.Builder stateBoolPropBuilder =
+ BoolProp.newBuilder()
+ .setValue(true)
+ .setDynamicValue(
+ DynamicBool.newBuilder()
+ .setStateSource(
+ StateBoolSource.newBuilder()
+ .setSourceKey(boolKey)));
+ LayoutElement.Builder boxBuilder =
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .addContents(image)
+ .setModifiers(
+ Modifiers.newBuilder()
+ .setHidden(stateBoolPropBuilder)));
LayoutElement root =
LayoutElement.newBuilder()
- .setRow(
- Row.newBuilder()
- .addContents(boxBuilder)
- .addContents(image))
+ .setRow(Row.newBuilder().addContents(boxBuilder).addContents(image))
.build();
FrameLayout layout = renderer(fingerprintedLayout(root)).inflate();
@@ -5059,7 +5208,8 @@
assertThat(secondImage.getLeft()).isEqualTo(secondImageLeft);
}
- @Test public void inflate_box_withVisibleModifier() {
+ @Test
+ public void inflate_box_withVisibleModifier() {
final String protoResId = "android";
final String boolKey = "bool-key";
@@ -5226,20 +5376,18 @@
ContainerDimension.newBuilder().setLinearDimension(dp(100.f).build()).build();
ContainerDimension innerBoxSize =
ContainerDimension.newBuilder().setLinearDimension(dp(60.f).build()).build();
- Box.Builder boxBuilder = Box.newBuilder()
- .setWidth(expand())
- .setHeight(wrap())
- .setModifiers(
- Modifiers.newBuilder()
- .setTransformation(
- transformation)
- .build())
- .addContents(
- LayoutElement.newBuilder()
- .setBox(
- Box.newBuilder()
- .setWidth(innerBoxSize)
- .setHeight(innerBoxSize)));
+ Box.Builder boxBuilder =
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(wrap())
+ .setModifiers(
+ Modifiers.newBuilder().setTransformation(transformation).build())
+ .addContents(
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(innerBoxSize)
+ .setHeight(innerBoxSize)));
LayoutElement root =
LayoutElement.newBuilder()
.setBox(
@@ -5657,7 +5805,7 @@
renderer.computeMutation(
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(textFadeIn("World")));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
// Idle for running code for starting animations.
@@ -5689,7 +5837,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 0)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5715,7 +5863,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 1)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5738,7 +5886,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 1)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5764,7 +5912,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 10)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5792,7 +5940,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 10)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
shadowOf(getMainLooper()).idle();
@@ -5821,7 +5969,7 @@
fingerprintedLayout(
getMultipleTextElementWithExitAnimation(
Arrays.asList("Hello"), /* iterations= */ 10)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5850,7 +5998,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 10)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5863,7 +6011,7 @@
getTextElementWithExitAnimation(
"Second mutation", /* iterations= */ 10)));
- ListenableFuture<Void> applySecondMutationFuture =
+ ListenableFuture<RenderingArtifact> applySecondMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, secondMutation);
// the previous mutation should be finished
@@ -6222,9 +6370,12 @@
private static Spacer.Builder buildExpandedSpacer(int widthWeight, int heightWeight) {
return Spacer.newBuilder()
- .setWidth(SpacerDimension.newBuilder().setExpandedDimension(expandWithWeight(widthWeight)))
+ .setWidth(
+ SpacerDimension.newBuilder()
+ .setExpandedDimension(expandWithWeight(widthWeight)))
.setHeight(
- SpacerDimension.newBuilder().setExpandedDimension(expandWithWeight(heightWeight)));
+ SpacerDimension.newBuilder()
+ .setExpandedDimension(expandWithWeight(heightWeight)));
}
private static ExpandedDimensionProp expandWithWeight(int weight) {
@@ -6255,4 +6406,15 @@
.addContents(LayoutElement.newBuilder().setSpacer(spacer)))
.build());
}
+
+ private static Text createTextWithVisibility(
+ String text, String id, Action action, boolean visibility) {
+ return Text.newBuilder()
+ .setText(string(text))
+ .setModifiers(
+ Modifiers.newBuilder()
+ .setVisible(BoolProp.newBuilder().setValue(visibility))
+ .setClickable(Clickable.newBuilder().setId(id).setOnClick(action)))
+ .build();
+ }
}
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 4a563e0..2d88c40 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -49,12 +49,13 @@
*
* To modify the user style, you should call [toMutableUserStyle] and construct a new [UserStyle]
* instance with [MutableUserStyle.toUserStyle].
- *
+ */
+public class UserStyle
+/**
* @param selectedOptions The [UserStyleSetting.Option] selected for each [UserStyleSetting]
* @param copySelectedOptions Whether to create a copy of the provided [selectedOptions]. If
* `false`, no mutable copy of the [selectedOptions] map should be retained outside this class.
*/
-public class UserStyle
private constructor(
selectedOptions: Map<UserStyleSetting, UserStyleSetting.Option>,
copySelectedOptions: Boolean
@@ -70,6 +71,8 @@
*
* A copy of the [selectedOptions] map will be created, so that changed to the map will not be
* reflected by this object.
+ *
+ * @param selectedOptions The [UserStyleSetting.Option] selected for each [UserStyleSetting]
*/
public constructor(
selectedOptions: Map<UserStyleSetting, UserStyleSetting.Option>
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 5d3c6df..93e468f 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -131,6 +131,7 @@
* @param boundsType The [ComplicationSlotBoundsTypeIntDef] of the complication
* @param zonedDateTime The [ZonedDateTime] to render the highlight with
* @param color The color to render the highlight with
+ * @param boundingArc Optional [BoundingArc] defining the geometry of an edge complication
*/
@ComplicationExperimental
public fun drawHighlight(
@@ -633,8 +634,12 @@
* @param defaultDataSourcePolicy The [DefaultComplicationDataSourcePolicy] used to select
* the initial complication data source when the watch is first installed.
* @param bounds The complication's [ComplicationSlotBounds]. Its likely the bounding rect
- * will be much larger than the complication and shouldn't directly be used for hit
+ * will have a much larger area than [boundingArc] and shouldn't directly be used for hit
* testing.
+ * @param boundingArc The [BoundingArc] defining the geometry of the edge complication.
+ * @param complicationTapFilter The [ComplicationTapFilter] used to determine whether or not
+ * a tap hit the complication. The default [ComplicationTapFilter] uses [boundingArc] to
+ * perform hit testing.
*/
@JvmStatic
@JvmOverloads
diff --git a/webkit/webkit/src/main/java/androidx/webkit/CookieManagerCompat.java b/webkit/webkit/src/main/java/androidx/webkit/CookieManagerCompat.java
index 3aaad88..cb31832 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/CookieManagerCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/CookieManagerCompat.java
@@ -39,6 +39,7 @@
* <a href="https://httpwg.org/specs/rfc6265.html#sane-set-cookie-syntax">the RFC6265 spec.</a>
* eg. "name=value; domain=.example.com; path=/"
*
+ * @param cookieManager The CookieManager instance to get info from.
* @param url the URL for which the API retrieves all available cookies.
* @return the cookies as a list of strings.
*/
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index 46d466f..5c15f3a 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -119,6 +119,7 @@
* {@link WebViewFeature#isFeatureSupported(String)}
* returns true for {@link WebViewFeature#SAFE_BROWSING_ENABLE}.
*
+ * @param settings The WebSettings object to update.
* @param enabled Whether Safe Browsing is enabled.
*/
@RequiresFeature(name = WebViewFeature.SAFE_BROWSING_ENABLE,
@@ -179,6 +180,7 @@
* {@link WebViewFeature#isFeatureSupported(String)}
* returns true for {@link WebViewFeature#DISABLED_ACTION_MODE_MENU_ITEMS}.
*
+ * @param settings The WebSettings object to update.
* @param menuItems an integer field flag for the menu items to be disabled.
*/
@RequiresFeature(name = WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS,
@@ -425,6 +427,7 @@
* is created.
*
* <p>
+ * @param settings The WebSettings object to update.
* @param allow allow algorithmic darkening or not.
*
*/
@@ -593,6 +596,7 @@
* {@link WebViewFeature#isFeatureSupported(String)}
* returns true for {@link WebViewFeature#ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY}.
*
+ * @param settings The WebSettings object to update.
* @param enabled Whether EnterpriseAuthenticationAppLinkPolicy should be enabled.
*/
@RequiresFeature(name = WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index d3f6250..de2c728 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -160,6 +160,7 @@
* {@link WebViewFeature#isFeatureSupported(String)}
* returns true for {@link WebViewFeature#VISUAL_STATE_CALLBACK}.
*
+ * @param webview The WebView to post to.
* @param requestId An id that will be returned in the callback to allow callers to match
* requests with callbacks.
* @param callback The callback to be invoked.
@@ -497,6 +498,7 @@
* }
* </pre
*
+ * @param webview The WebView to post to.
* @param message the WebMessage
* @param targetOrigin the target origin.
*/
@@ -760,6 +762,7 @@
* This method should only be called if {@link WebViewFeature#isFeatureSupported(String)}
* returns true for {@link WebViewFeature#WEB_MESSAGE_LISTENER}.
*
+ * @param webview The WebView object to remove from.
* @param jsObjectName The JavaScript object's name that was previously passed to {@link
* #addWebMessageListener(WebView, String, Set, WebMessageListener)}.
*